pax_global_header00006660000000000000000000000064147645135710014527gustar00rootroot0000000000000052 comment=c52cf2b0458e9c3082fc30cf260a87212d1a67b5 django-graphene-3.2.3/000077500000000000000000000000001476451357100145655ustar00rootroot00000000000000django-graphene-3.2.3/.coveragerc000066400000000000000000000000631476451357100167050ustar00rootroot00000000000000[run] omit = */tests/*,graphene_django/debug/sql/* django-graphene-3.2.3/.github/000077500000000000000000000000001476451357100161255ustar00rootroot00000000000000django-graphene-3.2.3/.github/ISSUE_TEMPLATE/000077500000000000000000000000001476451357100203105ustar00rootroot00000000000000django-graphene-3.2.3/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000016051476451357100230040ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: "\U0001F41Bbug" assignees: '' --- **Note: for support questions, please use stackoverflow**. This repository's issues are reserved for feature requests and bug reports. * **What is the current behavior?** * **If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem** via a github repo, https://repl.it or similar (you can use this template as a starting point: https://repl.it/@jkimbo/Graphene-Django-Example). * **What is the expected behavior?** * **What is the motivation / use case for changing the behavior?** * **Please tell us about your environment:** - Version: - Platform: * **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow) django-graphene-3.2.3/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000000341476451357100222750ustar00rootroot00000000000000blank_issues_enabled: false django-graphene-3.2.3/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011411476451357100240320ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: "✨enhancement" assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. django-graphene-3.2.3/.github/stale.yml000066400000000000000000000014241476451357100177610ustar00rootroot00000000000000# Number of days of inactivity before an issue becomes stale daysUntilStale: false # Number of days of inactivity before a stale issue is closed daysUntilClose: false # Issues with these labels will never be considered stale exemptLabels: - pinned - security - 🐛bug - 📖 documentation - help wanted - ✨enhancement # Label to use when marking an issue as stale staleLabel: wontfix # Comment to post when marking an issue as stale. Set to `false` to disable markComment: false # markComment: > # This issue has been automatically marked as stale because it has not had # recent activity. It will be closed if no further activity occurs. Thank you # for your contributions. # Comment to post when closing a stale issue. Set to `false` to disable closeComment: false django-graphene-3.2.3/.github/workflows/000077500000000000000000000000001476451357100201625ustar00rootroot00000000000000django-graphene-3.2.3/.github/workflows/deploy.yml000066400000000000000000000012621476451357100222020ustar00rootroot00000000000000name: 🚀 Deploy to PyPI on: push: tags: - 'v*' jobs: lint: uses: ./.github/workflows/lint.yml tests: uses: ./.github/workflows/tests.yml release: runs-on: ubuntu-latest needs: [lint, tests] steps: - uses: actions/checkout@v3 - name: Set up Python 3.11 uses: actions/setup-python@v4 with: python-version: '3.11' - name: Build wheel and source tarball run: | pip install wheel python setup.py sdist bdist_wheel - name: Publish a Python distribution to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.pypi_password }} django-graphene-3.2.3/.github/workflows/lint.yml000066400000000000000000000007341476451357100216570ustar00rootroot00000000000000name: Lint on: push: branches: ["main"] pull_request: workflow_call: jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python 3.11 uses: actions/setup-python@v4 with: python-version: '3.11' - name: Install dependencies run: | python -m pip install --upgrade pip pip install tox - name: Run pre-commit 💅 run: tox env: TOXENV: pre-commit django-graphene-3.2.3/.github/workflows/tests.yml000066400000000000000000000020661476451357100220530ustar00rootroot00000000000000name: Tests on: push: branches: ["main"] pull_request: workflow_call: jobs: build: runs-on: ubuntu-latest strategy: max-parallel: 4 matrix: django: ["3.2", "4.2", "5.0", "5.1"] python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] exclude: - django: "3.2" python-version: "3.11" - django: "3.2" python-version: "3.12" - django: "5.0" python-version: "3.8" - django: "5.0" python-version: "3.9" - django: "5.1" python-version: "3.8" - django: "5.1" python-version: "3.9" steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install tox tox-gh-actions - name: Test with tox run: tox env: DJANGO: ${{ matrix.django }} django-graphene-3.2.3/.gitignore000066400000000000000000000022261476451357100165570ustar00rootroot00000000000000# Created by https://www.gitignore.io ### Python ### # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ .env/ venv/ .venv/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ # PyCharm .idea # Databases *.sqlite3 .vscode # swap [._]*.s[a-v][a-z] [._]*.sw[a-p] [._]s[a-v][a-z] [._]sw[a-p] # session Session.vim # temporary .netrwhist *~ # auto-generated tag files tags .tox/ .pytest_cache/ # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: .python-version django-graphene-3.2.3/.pre-commit-config.yaml000066400000000000000000000011421476451357100210440ustar00rootroot00000000000000default_language_version: python: python3.11 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - id: check-merge-conflict - id: check-json - id: check-yaml - id: debug-statements - id: end-of-file-fixer exclude: ^docs/.*$ - id: pretty-format-json args: - --autofix - id: trailing-whitespace exclude: README.md - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.1.2 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix, --show-fixes] - id: ruff-format django-graphene-3.2.3/.readthedocs.yaml000066400000000000000000000006641476451357100200220ustar00rootroot00000000000000# .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details version: 2 build: os: ubuntu-22.04 tools: python: "3.12" # Build documentation in the "docs/" directory with Sphinx sphinx: configuration: docs/conf.py # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html python: install: - requirements: docs/requirements.txt django-graphene-3.2.3/.ruff.toml000066400000000000000000000014651476451357100165100ustar00rootroot00000000000000select = [ "E", # pycodestyle "W", # pycodestyle "F", # pyflake "I", # isort "B", # flake8-bugbear "C4", # flake8-comprehensions "UP", # pyupgrade ] ignore = [ "E501", # line-too-long "B017", # pytest.raises(Exception) should be considered evil "B028", # warnings.warn called without an explicit stacklevel keyword argument "B904", # check for raise statements in exception handlers that lack a from clause "W191", # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules ] exclude = [ "**/docs", ] target-version = "py38" [per-file-ignores] # Ignore unused imports (F401) in these files "__init__.py" = ["F401"] [isort] known-first-party = ["graphene", "graphene-django"] known-local-folder = ["cookbook"] combine-as-imports = true django-graphene-3.2.3/CODE_OF_CONDUCT.md000066400000000000000000000064261476451357100173740ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at me@syrusakbary.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq django-graphene-3.2.3/CONTRIBUTING.md000066400000000000000000000025351476451357100170230ustar00rootroot00000000000000# Contributing Thanks for helping to make graphene-django great! We welcome all kinds of contributions: - Bug fixes - Documentation improvements - New features - Refactoring & tidying ## Getting started 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. ## Project setup After cloning this repo, ensure dependencies are installed by running: ```sh make dev-setup ``` ## Running tests After developing, the full test suite can be evaluated by running: ```sh make tests ``` ## Opening Pull Requests Please fork the project and open a pull request against the `main` branch. This will trigger a series of test and lint checks. We advise that you format and run lint locally before doing this to save time: ```sh make format make lint ``` ## Documentation 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. The documentation dependencies are installed by running: ```sh cd docs pip install -r requirements.txt ``` Then to produce a HTML version of the documentation: ```sh make html ``` django-graphene-3.2.3/LICENSE000066400000000000000000000020771476451357100156000ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2016-Present Syrus Akbary Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. django-graphene-3.2.3/MANIFEST.in000066400000000000000000000004141476451357100163220ustar00rootroot00000000000000include README.md LICENSE recursive-include graphene_django/templates * recursive-include graphene_django/static * include examples/cookbook/cookbook/ingredients/fixtures/ingredients.json include examples/cookbook-plain/cookbook/ingredients/fixtures/ingredients.json django-graphene-3.2.3/Makefile000066400000000000000000000014471476451357100162330ustar00rootroot00000000000000.PHONY: help help: @echo "Please use \`make ' where is one of" @grep -E '^\.PHONY: [a-zA-Z_-]+ .*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = "(: |##)"}; {printf "\033[36m%-30s\033[0m %s\n", $$2, $$3}' .PHONY: dev-setup ## Install development dependencies dev-setup: pip install -e ".[dev]" python -m pre_commit install .PHONY: tests ## Run unit tests tests: PYTHONPATH=. pytest graphene_django --cov=graphene_django -vv .PHONY: format ## Format code format: ruff format graphene_django examples setup.py .PHONY: lint ## Lint code lint: ruff graphene_django examples .PHONY: docs ## Generate docs docs: dev-setup cd docs && make install && make html .PHONY: docs-live ## Generate docs with live reloading docs-live: dev-setup cd docs && make install && make livehtml django-graphene-3.2.3/README.md000066400000000000000000000135171476451357100160530ustar00rootroot00000000000000# ![Graphene Logo](http://graphene-python.org/favicon.png) Graphene-Django [![build][build-image]][build-url] [![pypi][pypi-image]][pypi-url] [![Anaconda-Server Badge][conda-image]][conda-url] [![coveralls][coveralls-image]][coveralls-url] [build-image]: https://github.com/graphql-python/graphene-django/workflows/Tests/badge.svg [build-url]: https://github.com/graphql-python/graphene-django/actions [pypi-image]: https://img.shields.io/pypi/v/graphene-django.svg?style=flat [pypi-url]: https://pypi.org/project/graphene-django/ [coveralls-image]: https://coveralls.io/repos/github/graphql-python/graphene-django/badge.svg?branch=master [coveralls-url]: https://coveralls.io/github/graphql-python/graphene-django?branch=master [conda-image]: https://img.shields.io/conda/vn/conda-forge/graphene-django.svg [conda-url]: https://anaconda.org/conda-forge/graphene-django Graphene-Django is an open-source library that provides seamless integration between Django, a high-level Python web framework, and Graphene, a library for building GraphQL APIs. The library allows developers to create GraphQL APIs in Django quickly and efficiently while maintaining a high level of performance. ## Features * Seamless integration with Django models * Automatic generation of GraphQL schema * Integration with Django's authentication and permission system * Easy querying and filtering of data * Support for Django's pagination system * Compatible with Django's form and validation system * Extensive documentation and community support ## Installation To install Graphene-Django, run the following command: ```sh pip install graphene-django ``` ## Configuration After installation, add 'graphene_django' to your Django project's `INSTALLED_APPS` list and define the GraphQL schema in your project's settings: ```python INSTALLED_APPS = [ # ... 'graphene_django', ] GRAPHENE = { 'SCHEMA': 'myapp.schema.schema' } ``` ## Usage To use Graphene-Django, create a `schema.py` file in your Django app directory and define your GraphQL types and queries: ```python import graphene from graphene_django import DjangoObjectType from .models import MyModel class MyModelType(DjangoObjectType): class Meta: model = MyModel class Query(graphene.ObjectType): mymodels = graphene.List(MyModelType) def resolve_mymodels(self, info, **kwargs): return MyModel.objects.all() schema = graphene.Schema(query=Query) ``` Then, expose the GraphQL API in your Django project's `urls.py` file: ```python from django.urls import path from graphene_django.views import GraphQLView from . import schema urlpatterns = [ # ... path('graphql/', GraphQLView.as_view(graphiql=True)), # Given that schema path is defined in GRAPHENE['SCHEMA'] in your settings.py ] ``` ## Testing Graphene-Django provides support for testing GraphQL APIs using Django's test client. To create tests, create a `tests.py` file in your Django app directory and write your test cases: ```python from django.test import TestCase from graphene_django.utils.testing import GraphQLTestCase from . import schema class MyModelAPITestCase(GraphQLTestCase): GRAPHENE_SCHEMA = schema.schema def test_query_all_mymodels(self): response = self.query( ''' query { mymodels { id name } } ''' ) self.assertResponseNoErrors(response) self.assertEqual(len(response.data['mymodels']), MyModel.objects.count()) ``` ## Contributing Contributions to Graphene-Django are always welcome! To get started, check the repository's [issue tracker](https://github.com/graphql-python/graphene-django/issues) and [contribution guidelines](https://github.com/graphql-python/graphene-django/blob/main/CONTRIBUTING.md). ## License Graphene-Django is released under the [MIT License](https://github.com/graphql-python/graphene-django/blob/main/LICENSE). ## Resources * [Official GitHub Repository](https://github.com/graphql-python/graphene-django) * [Graphene Documentation](http://docs.graphene-python.org/en/latest/) * [Django Documentation](https://docs.djangoproject.com/en/stable/) * [GraphQL Specification](https://spec.graphql.org/) * [GraphiQL](https://github.com/graphql/graphiql) - An in-browser IDE for exploring GraphQL APIs * [Graphene-Django Community](https://spectrum.chat/graphene) - Join the community to discuss questions and share ideas related to Graphene-Django ## Tutorials and Examples * [Official Graphene-Django Tutorial](https://docs.graphene-python.org/projects/django/en/latest/tutorial-plain/) * [Building a GraphQL API with Django and Graphene-Django](https://www.howtographql.com/graphql-python/0-introduction/) * [Real-world example: Django, Graphene, and Relay](https://github.com/graphql-python/swapi-graphene) ## Related Projects * [Graphene](https://github.com/graphql-python/graphene) - A library for building GraphQL APIs in Python * [Graphene-SQLAlchemy](https://github.com/graphql-python/graphene-sqlalchemy) - Integration between Graphene and SQLAlchemy, an Object Relational Mapper (ORM) for Python * [Graphene-File-Upload](https://github.com/lmcgartland/graphene-file-upload) - A package providing an Upload scalar for handling file uploads in Graphene * [Graphene-Subscriptions](https://github.com/graphql-python/graphene-subscriptions) - A package for adding real-time subscriptions to Graphene-based GraphQL APIs ## Support If you encounter any issues or have questions regarding Graphene-Django, feel free to [submit an issue](https://github.com/graphql-python/graphene-django/issues/new) on the official GitHub repository. You can also ask for help and share your experiences with the Graphene-Django community on [💬 Discord](https://discord.gg/Fftt273T79) ## Release Notes * See [Releases page on github](https://github.com/graphql-python/graphene-django/releases) django-graphene-3.2.3/bin/000077500000000000000000000000001476451357100153355ustar00rootroot00000000000000django-graphene-3.2.3/bin/autolinter000077500000000000000000000005261476451357100174540ustar00rootroot00000000000000#!/bin/bash # Install the required scripts with # pip install autoflake autopep8 isort autoflake ./examples/ ./graphene_django/ -r --remove-unused-variables --remove-all-unused-imports --in-place autopep8 ./examples/ ./graphene_django/ -r --in-place --experimental --aggressive --max-line-length 120 isort -rc ./examples/ ./graphene_django/ django-graphene-3.2.3/bin/convert_documentation000077500000000000000000000001101476451357100216640ustar00rootroot00000000000000#!/bin/bash pandoc README.md --from markdown --to rst -s -o README.rst django-graphene-3.2.3/docs/000077500000000000000000000000001476451357100155155ustar00rootroot00000000000000django-graphene-3.2.3/docs/Makefile000066400000000000000000000173241476451357100171640ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " epub3 to make an epub3" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" @echo " dummy to check syntax errors of document sources" .PHONY: clean clean: rm -rf $(BUILDDIR)/* .PHONY: install ## to install all documentation related requirements install: pip install -r requirements.txt .PHONY: html html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." .PHONY: livehtml ## to build and serve live-reloading documentation livehtml: sphinx-autobuild -b html --watch ../graphene_django $(ALLSPHINXOPTS) $(BUILDDIR)/html .PHONY: dirhtml dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." .PHONY: singlehtml singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." .PHONY: pickle pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." .PHONY: json json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." .PHONY: htmlhelp htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." .PHONY: qthelp qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Graphene.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Graphene.qhc" .PHONY: applehelp applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." .PHONY: devhelp devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Graphene" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Graphene" @echo "# devhelp" .PHONY: epub epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." .PHONY: epub3 epub3: $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 @echo @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." .PHONY: latex latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." .PHONY: latexpdf latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: latexpdfja latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: text text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." .PHONY: man man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." .PHONY: texinfo texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." .PHONY: info info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." .PHONY: gettext gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." .PHONY: changes changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." .PHONY: linkcheck linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." .PHONY: doctest doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." .PHONY: coverage coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." .PHONY: xml xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." .PHONY: pseudoxml pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." .PHONY: dummy dummy: $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy @echo @echo "Build finished. Dummy builder generates no files." django-graphene-3.2.3/docs/_static/000077500000000000000000000000001476451357100171435ustar00rootroot00000000000000django-graphene-3.2.3/docs/_static/.gitkeep000066400000000000000000000000001476451357100205620ustar00rootroot00000000000000django-graphene-3.2.3/docs/authorization.rst000066400000000000000000000147531476451357100211610ustar00rootroot00000000000000Authorization in Django ======================= There are several ways you may want to limit access to data when working with Graphene and Django: limiting which fields are accessible via GraphQL and limiting which objects a user can access. Let's use a simple example model. .. code:: python from django.db import models class Post(models.Model): title = models.CharField(max_length=100) content = models.TextField() published = models.BooleanField(default=False) owner = models.ForeignKey('auth.User') Limiting Field Access --------------------- To limit fields in a GraphQL query simply use the ``fields`` meta attribute. .. code:: python from graphene import relay from graphene_django.types import DjangoObjectType from .models import Post class PostNode(DjangoObjectType): class Meta: model = Post fields = ('title', 'content') interfaces = (relay.Node, ) conversely you can use ``exclude`` meta attribute. .. code:: python from graphene import relay from graphene_django.types import DjangoObjectType from .models import Post class PostNode(DjangoObjectType): class Meta: model = Post exclude = ('published', 'owner') interfaces = (relay.Node, ) Another pattern is to have a resolve method act as a gatekeeper, returning None or raising an exception if the client isn't allowed to see the data. .. code:: python from graphene import relay from graphene_django.types import DjangoObjectType from .models import Post class PostNode(DjangoObjectType): class Meta: model = Post fields = ('title', 'content', 'owner') interfaces = (relay.Node, ) def resolve_owner(self, info): user = info.context.user if user.is_anonymous: raise PermissionDenied("Please login") if not user.is_staff: return None return self.owner Queryset Filtering On Lists --------------------------- In order to filter which objects are available in a queryset-based list, define a resolve method for that field and return the desired queryset. .. code:: python from graphene import ObjectType from graphene_django.filter import DjangoFilterConnectionField from .models import Post class Query(ObjectType): all_posts = DjangoFilterConnectionField(PostNode) def resolve_all_posts(self, info): return Post.objects.filter(published=True) User-based Queryset Filtering ----------------------------- If you are using ``GraphQLView`` you can access Django's request with the context argument. .. code:: python from graphene import ObjectType from graphene_django.filter import DjangoFilterConnectionField from .models import Post class Query(ObjectType): my_posts = DjangoFilterConnectionField(PostNode) def resolve_my_posts(self, info): # context will reference to the Django request if not info.context.user.is_authenticated: return Post.objects.none() else: return Post.objects.filter(owner=info.context.user) If you're using your own view, passing the request context into the schema is simple. .. code:: python result = schema.execute(query, context_value=request) Global Filtering ---------------- If you are using ``DjangoObjectType`` you can define a custom `get_queryset`. .. code:: python from graphene import relay from graphene_django.types import DjangoObjectType from .models import Post class PostNode(DjangoObjectType): class Meta: model = Post fields = '__all__' @classmethod def get_queryset(cls, queryset, info): if info.context.user.is_anonymous: return queryset.filter(published=True) return queryset .. warning:: Defining a custom ``get_queryset`` gives the guaranteed it will be called when resolving the ``DjangoObjectType``, even through related objects. Note that because of this, benefits from using ``select_related`` in objects that define a relation to this ``DjangoObjectType`` will be canceled out. In the case of ``prefetch_related``, the benefits of the optimization will be lost only if the custom ``get_queryset`` modifies the queryset. For more information about this, refers to Django documentation about ``prefetch_related``: https://docs.djangoproject.com/en/4.2/ref/models/querysets/#prefetch-related. If you want to explicitly disable the execution of the custom ``get_queryset`` when resolving, you can decorate the resolver with `@graphene_django.bypass_get_queryset`. Note that this can lead to authorization leaks if you are performing authorization checks in the custom ``get_queryset``. Filtering ID-based Node Access ------------------------------ In order to add authorization to id-based node access, we need to add a method to your ``DjangoObjectType``. .. code:: python from graphene_django.types import DjangoObjectType from .models import Post class PostNode(DjangoObjectType): class Meta: model = Post fields = ('title', 'content') interfaces = (relay.Node, ) @classmethod def get_node(cls, info, id): try: post = cls._meta.model.objects.get(id=id) except cls._meta.model.DoesNotExist: return None if post.published or info.context.user == post.owner: return post return None Adding Login Required --------------------- To restrict users from accessing the GraphQL API page the standard Django LoginRequiredMixin_ can be used to create your own standard Django Class Based View, which includes the ``LoginRequiredMixin`` and subclasses the ``GraphQLView``.: .. code:: python # views.py from django.contrib.auth.mixins import LoginRequiredMixin from graphene_django.views import GraphQLView class PrivateGraphQLView(LoginRequiredMixin, GraphQLView): pass After this, you can use the new ``PrivateGraphQLView`` in the project's URL Configuration file ``url.py``: For Django 2.2 and above: .. code:: python urlpatterns = [ # some other urls path('graphql/', PrivateGraphQLView.as_view(graphiql=True, schema=schema)), ] .. _LoginRequiredMixin: https://docs.djangoproject.com/en/dev/topics/auth/default/#the-loginrequired-mixin django-graphene-3.2.3/docs/conf.py000066400000000000000000000304101476451357100170120ustar00rootroot00000000000000import os on_rtd = os.environ.get("READTHEDOCS", None) == "True" # -*- coding: utf-8 -*- # # Graphene documentation build configuration file, created by # sphinx-quickstart on Sun Sep 11 18:30:51 2016. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.todo", "sphinx.ext.coverage", "sphinx.ext.viewcode", ] if not on_rtd: extensions += ["sphinx.ext.githubpages"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = ".rst" # The encoding of source files. # # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = "Graphene Django" copyright = "Graphene 2017" author = "Syrus Akbary" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = "1.0" # The full version, including alpha/beta/rc tags. release = "1.0.dev" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # # today = '' # # Else, today_fmt is used as the format for a strftime call. # # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The reST default role (used for this markup: `text`) to use for all # documents. # # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # # html_theme = 'alabaster' # if on_rtd: # html_theme = 'sphinx_rtd_theme' import sphinx_graphene_theme html_theme = "sphinx_graphene_theme" html_theme_path = [sphinx_graphene_theme.get_html_theme_path()] # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. # " v documentation" by default. # # html_title = u'Graphene v1.0.dev' # A shorter title for the navigation bar. Default is the same as html_title. # # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # # html_logo = None # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # # html_extra_path = [] # If not None, a 'Last updated on:' timestamp is inserted at every page # bottom, using the given strftime format. # The empty string is equivalent to '%b %d, %Y'. # # html_last_updated_fmt = None # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # # html_additional_pages = {} # If false, no module index is generated. # # html_domain_indices = True # If false, no index is generated. # # html_use_index = True # If true, the index is split into individual pages for each letter. # # html_split_index = False # If true, links to the reST sources are added to the pages. # # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' # # html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # 'ja' uses this config value. # 'zh' user can custom change `jieba` dictionary path. # # html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. # # html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = "Graphenedoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, "Graphene.tex", "Graphene Documentation", "Syrus Akbary", "manual") ] # The name of an image file (relative to this directory) to place at the top of # the title page. # # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # # latex_use_parts = False # If true, show page references after internal links. # # latex_show_pagerefs = False # If true, show URL addresses after external links. # # latex_show_urls = False # Documents to append as an appendix to all manuals. # # latex_appendices = [] # It false, will not define \strong, \code, itleref, \crossref ... but only # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added # packages. # # latex_keep_old_macro_names = True # If false, no module index is generated. # # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, "graphene_django", "Graphene Django Documentation", [author], 1) ] # If true, show URL addresses after external links. # # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( master_doc, "Graphene-Django", "Graphene Django Documentation", author, "Graphene Django", "One line description of project.", "Miscellaneous", ) ] # Documents to append as an appendix to all manuals. # # texinfo_appendices = [] # If false, no module index is generated. # # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # # texinfo_no_detailmenu = False # -- Options for Epub output ---------------------------------------------- # Bibliographic Dublin Core info. epub_title = project epub_author = author epub_publisher = author epub_copyright = copyright # The basename for the epub file. It defaults to the project name. # epub_basename = project # The HTML theme for the epub output. Since the default themes are not # optimized for small screen space, using the same theme for HTML and epub # output is usually not wise. This defaults to 'epub', a theme designed to save # visual space. # # epub_theme = 'epub' # The language of the text. It defaults to the language option # or 'en' if the language is not set. # # epub_language = '' # The scheme of the identifier. Typical schemes are ISBN or URL. # epub_scheme = '' # The unique identifier of the text. This can be a ISBN number # or the project homepage. # # epub_identifier = '' # A unique identification for the text. # # epub_uid = '' # A tuple containing the cover image and cover page html template filenames. # # epub_cover = () # A sequence of (type, uri, title) tuples for the guide element of content.opf. # # epub_guide = () # HTML files that should be inserted before the pages created by sphinx. # The format is a list of tuples containing the path and title. # # epub_pre_files = [] # HTML files that should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. # # epub_post_files = [] # A list of files that should not be packed into the epub file. epub_exclude_files = ["search.html"] # The depth of the table of contents in toc.ncx. # # epub_tocdepth = 3 # Allow duplicate toc entries. # # epub_tocdup = True # Choose between 'default' and 'includehidden'. # # epub_tocscope = 'default' # Fix unsupported image types using the Pillow. # # epub_fix_images = False # Scale large images. # # epub_max_image_width = 0 # How to display URL addresses: 'footnote', 'no', or 'inline'. # # epub_show_urls = 'inline' # If false, no index is generated. # # epub_use_index = True # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { # "https://docs.python.org/": None, "python": ("https://docs.python.org/", None), } django-graphene-3.2.3/docs/debug.rst000066400000000000000000000032401476451357100173340ustar00rootroot00000000000000Django Debug Middleware ======================= You can debug your GraphQL queries in a similar way to `django-debug-toolbar `__, but outputting in the results in GraphQL response as fields, instead of the graphical HTML interface. Exceptions with their stack traces are also exposed. For that, you will need to add the plugin in your graphene schema. Installation ------------ For use the Django Debug plugin in Graphene: * Add ``graphene_django.debug.DjangoDebugMiddleware`` into ``MIDDLEWARE`` in the ``GRAPHENE`` settings. * Add the ``debug`` field into the schema root ``Query`` with the value ``graphene.Field(DjangoDebug, name='_debug')``. .. code:: python from graphene_django.debug import DjangoDebug class Query(graphene.ObjectType): # ... debug = graphene.Field(DjangoDebug, name='_debug') schema = graphene.Schema(query=Query) And in your ``settings.py``: .. code:: python GRAPHENE = { ... 'MIDDLEWARE': [ 'graphene_django.debug.DjangoDebugMiddleware', ] } Querying -------- You can query it for outputting all the sql transactions that happened in the GraphQL request, like: .. code:: { # A example that will use the ORM for interact with the DB allIngredients { edges { node { id, name } } } # Here is the debug field that will output the SQL queries _debug { sql { rawSql } exceptions { message stack } } } Note that the ``_debug`` field must be the last field in your query. django-graphene-3.2.3/docs/extra-types.rst000066400000000000000000000004031476451357100205310ustar00rootroot00000000000000Extra Types =========== Here are some libraries that provide common types for Django specific fields. GeoDjango --------- Use the graphene-gis_ library to add GeoDjango types to your Schema. .. _graphene-gis: https://github.com/EverWinter23/graphene-gis django-graphene-3.2.3/docs/fields.rst000066400000000000000000000043311476451357100175160ustar00rootroot00000000000000Fields ====== Graphene-Django provides some useful fields to help integrate Django with your GraphQL Schema. DjangoListField --------------- ``DjangoListField`` allows you to define a list of :ref:`DjangoObjectType`'s. By default it will resolve the default queryset of the Django model. .. code:: python from graphene import ObjectType, Schema from graphene_django import DjangoListField class RecipeType(DjangoObjectType): class Meta: model = Recipe fields = ("title", "instructions") class Query(ObjectType): recipes = DjangoListField(RecipeType) schema = Schema(query=Query) The above code results in the following schema definition: .. code:: schema { query: Query } type Query { recipes: [RecipeType!] } type RecipeType { title: String! instructions: String! } Custom resolvers **************** If your ``DjangoObjectType`` has defined a custom :ref:`get_queryset` method, when resolving a ``DjangoListField`` it will be called with either the return of the field resolver (if one is defined) or the default queryset from the Django model. For example the following schema will only resolve recipes which have been published and have a title: .. code:: python from graphene import ObjectType, Schema from graphene_django import DjangoListField class RecipeType(DjangoObjectType): class Meta: model = Recipe fields = ("title", "instructions") @classmethod def get_queryset(cls, queryset, info): # Filter out recipes that have no title return queryset.exclude(title__exact="") class Query(ObjectType): recipes = DjangoListField(RecipeType) def resolve_recipes(parent, info): # Only get recipes that have been published return Recipe.objects.filter(published=True) schema = Schema(query=Query) DjangoConnectionField --------------------- ``DjangoConnectionField`` acts similarly to ``DjangoListField`` but returns a paginated connection following the `relay spec `__ The field supports the following arguments: `first`, `last`, `offset`, `after` & `before`. django-graphene-3.2.3/docs/filtering.rst000066400000000000000000000230261476451357100202350ustar00rootroot00000000000000Filtering ========= Graphene integrates with `django-filter `__ to provide filtering of results. See the `usage documentation `__ for details on the format for ``filter_fields``. This filtering is automatically available when implementing a ``relay.Node``. Additionally ``django-filter`` is an optional dependency of Graphene. You will need to install it manually, which can be done as follows: .. code:: bash # You'll need to install django-filter pip install django-filter>=2 After installing ``django-filter`` you'll need to add the application in the ``settings.py`` file: .. code:: python INSTALLED_APPS = [ # ... "django_filters", ] Note: The techniques below are demoed in the `cookbook example app `__. Filterable fields ----------------- The ``filter_fields`` parameter is used to specify the fields which can be filtered upon. The value specified here is passed directly to ``django-filter``, so see the `filtering documentation `__ for full details on the range of options available. For example: .. code:: python class AnimalNode(DjangoObjectType): class Meta: # Assume you have an Animal model defined with the following fields model = Animal fields = '__all__' filter_fields = ['name', 'genus', 'is_domesticated'] interfaces = (relay.Node, ) class Query(ObjectType): animal = relay.Node.Field(AnimalNode) all_animals = DjangoFilterConnectionField(AnimalNode) You could then perform a query such as: .. code:: query { # Note that fields names become camelcased allAnimals(genus: "cat", isDomesticated: true) { edges { node { id, name } } } } You can also make more complex lookup types available: .. code:: python class AnimalNode(DjangoObjectType): class Meta: model = Animal fields = '__all__' # Provide more complex lookup types filter_fields = { 'name': ['exact', 'icontains', 'istartswith'], 'genus': ['exact'], 'is_domesticated': ['exact'], } interfaces = (relay.Node, ) Which you could query as follows: .. code:: query { # Note that fields names become camelcased allAnimals(name_Icontains: "lion") { edges { node { id, name } } } } Custom Filtersets ----------------- By default Graphene provides easy access to the most commonly used features of ``django-filter``. This is done by transparently creating a ``django_filters.FilterSet`` class for you and passing in the values for ``filter_fields``. However, you may find this to be insufficient. In these cases you can create your own ``FilterSet``. You can pass it directly as follows: .. code:: python class AnimalNode(DjangoObjectType): class Meta: # Assume you have an Animal model defined with the following fields model = Animal fields = '__all__' filter_fields = ['name', 'genus', 'is_domesticated'] interfaces = (relay.Node, ) class AnimalFilter(django_filters.FilterSet): # Do case-insensitive lookups on 'name' name = django_filters.CharFilter(lookup_expr=['iexact']) # Allow multiple genera to be selected at once genera = django_filters.MultipleChoiceFilter( field_name='genus', choices=( ('Canis', 'Canis'), ('Panthera', 'Panthera'), ('Seahorse', 'Seahorse') ) ) class Meta: model = Animal fields = ['name', 'genus', 'is_domesticated'] class Query(ObjectType): animal = relay.Node.Field(AnimalNode) # We specify our custom AnimalFilter using the filterset_class param all_animals = DjangoFilterConnectionField(AnimalNode, filterset_class=AnimalFilter) If you were interested in selecting all dogs and cats, you might query as follows: .. code:: query { allAnimals(genera: ["Canis", "Panthera"]) { edges { node { id, name } } } } You can also specify the ``FilterSet`` class using the ``filterset_class`` parameter when defining your ``DjangoObjectType``, however, this can't be used in unison with the ``filter_fields`` parameter: .. code:: python class AnimalFilter(django_filters.FilterSet): # Do case-insensitive lookups on 'name' name = django_filters.CharFilter(lookup_expr=['iexact']) class Meta: # Assume you have an Animal model defined with the following fields model = Animal fields = ['name', 'genus', 'is_domesticated'] class AnimalNode(DjangoObjectType): class Meta: model = Animal fields = '__all__' filterset_class = AnimalFilter interfaces = (relay.Node, ) class Query(ObjectType): animal = relay.Node.Field(AnimalNode) all_animals = DjangoFilterConnectionField(AnimalNode) The context argument is passed on as the `request argument `__ in a ``django_filters.FilterSet`` instance. You can use this to customize your filters to be context-dependent. We could modify the ``AnimalFilter`` above to pre-filter animals owned by the authenticated user (set in ``context.user``). .. code:: python class AnimalFilter(django_filters.FilterSet): # Do case-insensitive lookups on 'name' name = django_filters.CharFilter(lookup_type=['iexact']) class Meta: model = Animal fields = ['name', 'genus', 'is_domesticated'] @property def qs(self): # The query context can be found in self.request. return super(AnimalFilter, self).qs.filter(owner=self.request.user) Ordering -------- You can use ``OrderFilter`` to define how you want your returned results to be ordered. Extend the tuple of fields if you want to order by more than one field. .. code:: python from django_filters import FilterSet, OrderingFilter class UserFilter(FilterSet): class Meta: model = UserModel order_by = OrderingFilter( fields=( ('name', 'created_at'), ) ) class Group(DjangoObjectType): users = DjangoFilterConnectionField(Ticket, filterset_class=UserFilter) class Meta: name = 'Group' model = GroupModel fields = '__all__' interfaces = (relay.Node,) def resolve_users(self, info, **kwargs): return UserFilter(kwargs).qs with this set up, you can now order the users under group: .. code:: query { group(id: "xxx") { users(orderBy: "-created_at") { xxx } } } PostgreSQL `ArrayField` ----------------------- Graphene provides an easy to implement filters on `ArrayField` as they are not natively supported by django_filters: .. code:: python from django.db import models from django_filters import FilterSet, OrderingFilter from graphene_django.filter import ArrayFilter class Event(models.Model): name = models.CharField(max_length=50) tags = ArrayField(models.CharField(max_length=50)) class EventFilterSet(FilterSet): class Meta: model = Event fields = { "name": ["exact", "contains"], } tags__contains = ArrayFilter(field_name="tags", lookup_expr="contains") tags__overlap = ArrayFilter(field_name="tags", lookup_expr="overlap") tags = ArrayFilter(field_name="tags", lookup_expr="exact") class EventType(DjangoObjectType): class Meta: model = Event interfaces = (Node,) fields = "__all__" filterset_class = EventFilterSet with this set up, you can now filter events by tags: .. code:: query { events(tags_Overlap: ["concert", "festival"]) { name } } `TypedFilter` ------------- Sometimes the automatic detection of the filter input type is not satisfactory for what you are trying to achieve. You can then explicitly specify the input type you want for your filter by using a `TypedFilter`: .. code:: python from django.db import models from django_filters import FilterSet, OrderingFilter import graphene from graphene_django.filter import TypedFilter class Event(models.Model): name = models.CharField(max_length=50) class EventFilterSet(FilterSet): class Meta: model = Event fields = { "name": ["exact", "contains"], } only_first = TypedFilter(input_type=graphene.Boolean, method="only_first_filter") def only_first_filter(self, queryset, _name, value): if value: return queryset[:1] else: return queryset class EventType(DjangoObjectType): class Meta: model = Event interfaces = (Node,) fields = "__all__" filterset_class = EventFilterSet django-graphene-3.2.3/docs/index.rst000066400000000000000000000017261476451357100173640ustar00rootroot00000000000000Graphene-Django =============== Welcome to the Graphene-Django docs. Graphene-Django is built on top of `Graphene `__. Graphene-Django provides some additional abstractions that make it easy to add GraphQL functionality to your Django project. First time? We recommend you start with the installation guide to get set up and the basic tutorial. It is worth reading the `core graphene docs `__ to familiarize yourself with the basic utilities. Core tenets ----------- If you want to expose your data through GraphQL - read the ``Installation``, ``Schema`` and ``Queries`` section. For more advanced use, check out the Relay tutorial. .. toctree:: :maxdepth: 1 installation tutorial-plain tutorial-relay schema queries fields extra-types mutations subscriptions filtering authorization debug introspection validation testing settings django-graphene-3.2.3/docs/installation.rst000066400000000000000000000047131476451357100207550ustar00rootroot00000000000000Installation ============ Graphene-Django takes a few seconds to install and set up. Requirements ------------ Graphene-Django currently supports the following versions of Django: * >= Django 2.2 Installation ------------ .. code:: bash pip install graphene-django **We strongly recommend pinning against a specific version of Graphene-Django because new versions could introduce breaking changes to your project.** Add ``graphene_django`` to the ``INSTALLED_APPS`` in the ``settings.py`` file of your Django project: .. code:: python INSTALLED_APPS = [ ... "django.contrib.staticfiles", # Required for GraphiQL "graphene_django" ] We need to add a ``graphql`` URL to the ``urls.py`` of your Django project: For Django 2.2 and above: .. code:: python from django.urls import path from graphene_django.views import GraphQLView urlpatterns = [ # ... path("graphql", GraphQLView.as_view(graphiql=True)), ] (Change ``graphiql=True`` to ``graphiql=False`` if you do not want to use the GraphiQL API browser.) Finally, define the schema location for Graphene in the ``settings.py`` file of your Django project: .. code:: python GRAPHENE = { "SCHEMA": "django_root.schema.schema" } Where ``path.schema.schema`` is the location of the ``Schema`` object in your Django project. The most basic ``schema.py`` looks like this: .. code:: python import graphene class Query(graphene.ObjectType): hello = graphene.String(default_value="Hi!") schema = graphene.Schema(query=Query) To learn how to extend the schema object for your project, read the basic tutorial. CSRF exempt ----------- If you have enabled `CSRF protection `_ in your Django app you will find that it prevents your API clients from POSTing to the ``graphql`` endpoint. You can either update your API client to pass the CSRF token with each request (the Django docs have a guide on how to do that: https://docs.djangoproject.com/en/3.0/ref/csrf/#ajax) or you can exempt your Graphql endpoint from CSRF protection by wrapping the ``GraphQLView`` with the ``csrf_exempt`` decorator: .. code:: python # urls.py from django.urls import path from django.views.decorators.csrf import csrf_exempt from graphene_django.views import GraphQLView urlpatterns = [ # ... path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True))), ] django-graphene-3.2.3/docs/introspection.rst000066400000000000000000000042501476451357100211500ustar00rootroot00000000000000Introspection Schema ==================== Relay Modern uses `Babel Relay Plugin `__ which requires you to provide your GraphQL schema data. Graphene comes with a Django management command to dump your schema data to ``schema.json`` which is compatible with babel-relay-plugin. Usage ----- Include ``graphene_django`` to ``INSTALLED_APPS`` in your project settings: .. code:: python INSTALLED_APPS += ('graphene_django') Assuming your Graphene schema is at ``tutorial.quickstart.schema``, run the command: .. code:: bash ./manage.py graphql_schema --schema tutorial.quickstart.schema --out schema.json It dumps your full introspection schema to ``schema.json`` inside your project root directory. Point ``babel-relay-plugin`` to this file and you're ready to use Relay with Graphene GraphQL implementation. The schema file is sorted to create a reproducible canonical representation. GraphQL SDL Representation -------------------------- The schema can also be exported as a GraphQL SDL file by changing the file extension : .. code:: bash ./manage.py graphql_schema --schema tutorial.quickstart.schema --out schema.graphql When exporting the schema as a ``.graphql`` file the ``--indent`` option is ignored. Advanced Usage -------------- The ``--indent`` option can be used to specify the number of indentation spaces to be used in the output. Defaults to `None` which displays all data on a single line. 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 To simplify the command to ``./manage.py graphql_schema``, you can specify the parameters in your settings.py: .. code:: python GRAPHENE = { 'SCHEMA': 'tutorial.quickstart.schema', 'SCHEMA_OUTPUT': 'data/schema.json', # defaults to schema.json, 'SCHEMA_INDENT': 2, # Defaults to None (displays all data on a single line) } Running ``./manage.py graphql_schema`` dumps your schema to ``/data/schema.json``. Help ---- Run ``./manage.py graphql_schema -h`` for command usage. django-graphene-3.2.3/docs/mutations.rst000066400000000000000000000306131476451357100202750ustar00rootroot00000000000000Mutations ========= Introduction ------------ Graphene-Django makes it easy to perform mutations. With Graphene-Django we can take advantage of pre-existing Django features to quickly build CRUD functionality, while still using the core `graphene mutation `__ features to add custom mutations to a Django project. Simple example -------------- .. code:: python import graphene from graphene_django import DjangoObjectType from .models import Question class QuestionType(DjangoObjectType): class Meta: model = Question fields = '__all__' class QuestionMutation(graphene.Mutation): class Arguments: # The input arguments for this mutation text = graphene.String(required=True) id = graphene.ID() # The class attributes define the response of the mutation question = graphene.Field(QuestionType) @classmethod def mutate(cls, root, info, text, id): question = Question.objects.get(pk=id) question.text = text question.save() # Notice we return an instance of this mutation return QuestionMutation(question=question) class Mutation(graphene.ObjectType): update_question = QuestionMutation.Field() Django Forms ------------ Graphene-Django comes with mutation classes that will convert the fields on Django forms into inputs on a mutation. DjangoFormMutation ~~~~~~~~~~~~~~~~~~ .. code:: python from graphene_django.forms.mutation import DjangoFormMutation class MyForm(forms.Form): name = forms.CharField() class MyMutation(DjangoFormMutation): class Meta: form_class = MyForm ``MyMutation`` will automatically receive an ``input`` argument. This argument should be a ``dict`` where the key is ``name`` and the value is a string. DjangoModelFormMutation ~~~~~~~~~~~~~~~~~~~~~~~ ``DjangoModelFormMutation`` will pull the fields from a ``ModelForm``. .. code:: python from graphene_django.forms.mutation import DjangoModelFormMutation class Pet(models.Model): name = models.CharField() class PetForm(forms.ModelForm): class Meta: model = Pet fields = ('name',) # This will get returned when the mutation completes successfully class PetType(DjangoObjectType): class Meta: model = Pet fields = '__all__' class PetMutation(DjangoModelFormMutation): pet = Field(PetType) class Meta: form_class = PetForm ``PetMutation`` will grab the fields from ``PetForm`` and turn them into inputs. If the form is valid then the mutation will lookup the ``DjangoObjectType`` for the ``Pet`` model and return that under the key ``pet``. Otherwise it will return a list of errors. You can change the input name (default is ``input``) and the return field name (default is the model name lowercase). .. code:: python class PetMutation(DjangoModelFormMutation): class Meta: form_class = PetForm input_field_name = 'data' return_field_name = 'my_pet' Form validation ~~~~~~~~~~~~~~~ Form mutations will call ``is_valid()`` on your forms. If the form is valid then the class method ``perform_mutate(form, info)`` is called on the mutation. Override this method to change how the form is saved or to return a different Graphene object type. If the form is *not* valid then a list of errors will be returned. These errors have two fields: ``field``, a string containing the name of the invalid form field, and ``messages``, a list of strings with the validation messages. DjangoFormInputObjectType ~~~~~~~~~~~~~~~~~~~~~~~~~ ``DjangoFormInputObjectType`` is used in mutations to create input fields by **using django form** to retrieve input data structure from it. This can be helpful in situations where you need to pass data to several django forms in one mutation. .. code:: python from graphene_django.forms.types import DjangoFormInputObjectType class PetFormInput(DjangoFormInputObjectType): # any other fields can be placed here as well as # other djangoforminputobjects and intputobjects class Meta: form_class = PetForm object_type = PetType class QuestionFormInput(DjangoFormInputObjectType) class Meta: form_class = QuestionForm object_type = QuestionType class SeveralFormsInputData(graphene.InputObjectType): pet = PetFormInput(required=True) question = QuestionFormInput(required=True) class SomeSophisticatedMutation(graphene.Mutation): class Arguments: data = SeveralFormsInputData(required=True) @staticmethod def mutate(_root, _info, data): pet_form_inst = PetForm(data=data.pet) question_form_inst = QuestionForm(data=data.question) if pet_form_inst.is_valid(): pet_model_instance = pet_form_inst.save(commit=False) if question_form_inst.is_valid(): question_model_instance = question_form_inst.save(commit=False) # ... Additional to **InputObjectType** ``Meta`` class attributes: * ``form_class`` is required and should be equal to django form class. * ``object_type`` is not required and used to enable convertion of enum values back to original if model object type ``convert_choices_to_enum`` ``Meta`` class attribute is not set to ``False``. Any data field, which have choices in django, with value ``A_1`` (for example) from client will be automatically converted to ``1`` in mutation data. * ``add_id_field_name`` is used to specify `id` field name (not required, by default equal to ``id``) * ``add_id_field_type`` is used to specify `id` field type (not required, default is ``graphene.ID``) Django REST Framework --------------------- You can re-use your Django Rest Framework serializer with Graphene Django mutations. You can create a Mutation based on a serializer by using the `SerializerMutation` base class: .. code:: python from graphene_django.rest_framework.mutation import SerializerMutation class MyAwesomeMutation(SerializerMutation): class Meta: serializer_class = MySerializer Create/Update Operations ~~~~~~~~~~~~~~~~~~~~~~~~ By default ModelSerializers accept create and update operations. To customize this use the `model_operations` attribute on the ``SerializerMutation`` class. The update operation looks up models by the primary key by default. You can customize the look up with the ``lookup_field`` attribute on the ``SerializerMutation`` class. .. code:: python from graphene_django.rest_framework.mutation import SerializerMutation from .serializers import MyModelSerializer class AwesomeModelMutation(SerializerMutation): class Meta: serializer_class = MyModelSerializer model_operations = ['create', 'update'] lookup_field = 'id' Overriding Update Queries ~~~~~~~~~~~~~~~~~~~~~~~~~ Use the method ``get_serializer_kwargs`` to override how updates are applied. .. code:: python from graphene_django.rest_framework.mutation import SerializerMutation from .serializers import MyModelSerializer class AwesomeModelMutation(SerializerMutation): class Meta: serializer_class = MyModelSerializer @classmethod def get_serializer_kwargs(cls, root, info, **input): if 'id' in input: instance = Post.objects.filter( id=input['id'], owner=info.context.user ).first() if instance: return {'instance': instance, 'data': input, 'partial': True} else: raise http.Http404 return {'data': input, 'partial': True} Relay ----- You can use relay with mutations. A Relay mutation must inherit from ``ClientIDMutation`` and implement the ``mutate_and_get_payload`` method: .. code:: python import graphene from graphene import relay from graphene_django import DjangoObjectType from graphql_relay import from_global_id from .queries import QuestionType class QuestionMutation(relay.ClientIDMutation): class Input: text = graphene.String(required=True) id = graphene.ID() question = graphene.Field(QuestionType) @classmethod def mutate_and_get_payload(cls, root, info, text, id): question = Question.objects.get(pk=from_global_id(id)[1]) question.text = text question.save() return QuestionMutation(question=question) Notice that the ``class Arguments`` is renamed to ``class Input`` with relay. This is due to a deprecation of ``class Arguments`` in graphene 2.0. Relay ClientIDMutation accept a ``clientIDMutation`` argument. This argument is also sent back to the client with the mutation result (you do not have to do anything). For services that manage a pool of many GraphQL requests in bulk, the ``clientIDMutation`` allows you to match up a specific mutation with the response. Django Database Transactions ---------------------------- Django gives you a few ways to control how database transactions are managed. Tying transactions to HTTP requests ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A common way to handle transactions in Django is to wrap each request in a transaction. Set ``ATOMIC_REQUESTS`` settings to ``True`` in the configuration of each database for which you want to enable this behavior. It works like this. Before calling ``GraphQLView`` Django starts a transaction. If the response is produced without problems, Django commits the transaction. If the view, a ``DjangoFormMutation`` or a ``DjangoModelFormMutation`` produces an exception, Django rolls back the transaction. .. warning:: While the simplicity of this transaction model is appealing, it also makes it inefficient when traffic increases. Opening a transaction for every request has some overhead. The impact on performance depends on the query patterns of your application and on how well your database handles locking. Check the next section for a better solution. Tying transactions to mutations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A mutation can contain multiple fields, just like a query. There's one important distinction between queries and mutations, other than the name: .. `While query fields are executed in parallel, mutation fields run in series, one after the other.` This means that if we send two ``incrementCredits`` mutations in one request, the first is guaranteed to finish before the second begins, ensuring that we don't end up with a race condition with ourselves. On the other hand, if the first ``incrementCredits`` runs successfully but the second one does not, the operation cannot be retried as it is. That's why is a good idea to run all mutation fields in a transaction, to guarantee all occur or nothing occurs. To enable this behavior for all databases set the graphene ``ATOMIC_MUTATIONS`` settings to ``True`` in your settings file: .. code:: python GRAPHENE = { # ... "ATOMIC_MUTATIONS": True, } On the contrary, if you want to enable this behavior for a specific database, set ``ATOMIC_MUTATIONS`` to ``True`` in your database settings: .. code:: python DATABASES = { "default": { # ... "ATOMIC_MUTATIONS": True, }, # ... } Now, given the following example mutation: .. code:: mutation IncreaseCreditsTwice { increaseCredits1: increaseCredits(input: { amount: 10 }) { balance errors { field messages } } increaseCredits2: increaseCredits(input: { amount: -1 }) { balance errors { field messages } } } The server is going to return something like: .. code:: json { "data": { "increaseCredits1": { "balance": 10.0, "errors": [] }, "increaseCredits2": { "balance": null, "errors": [ { "field": "amount", "message": "Amount should be a positive number" } ] }, } } But the balance will remain the same. django-graphene-3.2.3/docs/queries.rst000066400000000000000000000311271476451357100177300ustar00rootroot00000000000000.. _queries-objecttypes: Queries & ObjectTypes ===================== Introduction ------------ Graphene-Django offers a host of features for performing GraphQL queries. Graphene-Django ships with a special ``DjangoObjectType`` that automatically transforms a Django Model into a ``ObjectType`` for you. Full example ~~~~~~~~~~~~ .. code:: python # my_app/schema.py import graphene from graphene_django import DjangoObjectType from .models import Question class QuestionType(DjangoObjectType): class Meta: model = Question fields = ("id", "question_text") class Query(graphene.ObjectType): questions = graphene.List(QuestionType) question_by_id = graphene.Field(QuestionType, id=graphene.String()) def resolve_questions(root, info, **kwargs): # Querying a list return Question.objects.all() def resolve_question_by_id(root, info, id): # Querying a single question return Question.objects.get(pk=id) Specifying which fields to include ---------------------------------- By default, ``DjangoObjectType`` will present all fields on a Model through GraphQL. If you only want a subset of fields to be present, you can do so using ``fields`` or ``exclude``. It is strongly recommended that you explicitly set all fields that should be exposed using the fields attribute. This will make it less likely to result in unintentionally exposing data when your models change. Setting neither ``fields`` nor ``exclude`` is deprecated and will raise a warning, you should at least explicitly make ``DjangoObjectType`` include all fields in the model as described below. ``fields`` ~~~~~~~~~~ Show **only** these fields on the model: .. code:: python from graphene_django import DjangoObjectType from .models import Question class QuestionType(DjangoObjectType): class Meta: model = Question fields = ("id", "question_text") You can also set the ``fields`` attribute to the special value ``"__all__"`` to indicate that all fields in the model should be used. For example: .. code:: python from graphene_django import DjangoObjectType from .models import Question class QuestionType(DjangoObjectType): class Meta: model = Question fields = "__all__" ``exclude`` ~~~~~~~~~~~ Show all fields **except** those in ``exclude``: .. code:: python from graphene_django import DjangoObjectType from .models import Question class QuestionType(DjangoObjectType): class Meta: model = Question exclude = ("question_text",) Customising fields ------------------ You can completely overwrite a field, or add new fields, to a ``DjangoObjectType`` using a Resolver: .. code:: python from graphene_django import DjangoObjectType from .models import Question class QuestionType(DjangoObjectType): class Meta: model = Question fields = ("id", "question_text") extra_field = graphene.String() def resolve_extra_field(self, info): return "hello!" Choices to Enum conversion ~~~~~~~~~~~~~~~~~~~~~~~~~~ By default Graphene-Django will convert any Django fields that have `choices`_ defined into a GraphQL enum type. .. _choices: https://docs.djangoproject.com/en/2.2/ref/models/fields/#choices For example the following ``Model`` and ``DjangoObjectType``: .. code:: python from django.db import models from graphene_django import DjangoObjectType class PetModel(models.Model): kind = models.CharField( max_length=100, choices=(("cat", "Cat"), ("dog", "Dog")) ) class Pet(DjangoObjectType): class Meta: model = PetModel fields = ("id", "kind",) Results in the following GraphQL schema definition: .. code:: graphql type Pet { id: ID! kind: PetModelKind! } enum PetModelKind { CAT DOG } You can disable this automatic conversion by setting ``convert_choices_to_enum`` attribute to ``False`` on the ``DjangoObjectType`` ``Meta`` class. .. code:: python from graphene_django import DjangoObjectType from .models import PetModel class Pet(DjangoObjectType): class Meta: model = PetModel fields = ("id", "kind",) convert_choices_to_enum = False .. code:: graphql type Pet { id: ID! kind: String! } You can also set ``convert_choices_to_enum`` to a list of fields that should be automatically converted into enums: .. code:: python from graphene_django import DjangoObjectType from .models import PetModel class Pet(DjangoObjectType): class Meta: model = PetModel fields = ("id", "kind",) convert_choices_to_enum = ["kind"] **Note:** Setting ``convert_choices_to_enum = []`` is the same as setting it to ``False``. Related models -------------- Say you have the following models: .. code:: python from django.db import models class Category(models.Model): foo = models.CharField(max_length=256) class Question(models.Model): category = models.ForeignKey(Category, on_delete=models.CASCADE) When ``Question`` is published as a ``DjangoObjectType`` and you want to add ``Category`` as a query-able field like so: .. code:: python from graphene_django import DjangoObjectType from .models import Question class QuestionType(DjangoObjectType): class Meta: model = Question fields = ("category",) Then all query-able related models must be defined as DjangoObjectType subclass, or they will fail to show if you are trying to query those relation fields. You only need to create the most basic class for this to work: .. code:: python from graphene_django import DjangoObjectType from .models import Category class CategoryType(DjangoObjectType): class Meta: model = Category fields = ("foo",) .. _django-objecttype-get-queryset: Default QuerySet ----------------- If you are using ``DjangoObjectType`` you can define a custom `get_queryset` method. Use this to control filtering on the ObjectType level instead of the Query object level. .. code:: python from graphene_django.types import DjangoObjectType from .models import Question class QuestionType(DjangoObjectType): class Meta: model = Question fields = "__all__" @classmethod def get_queryset(cls, queryset, info): if info.context.user.is_anonymous: return queryset.filter(published=True) return queryset Resolvers --------- When a GraphQL query is received by the ``Schema`` object, it will map it to a "Resolver" related to it. This resolve method should follow this format: .. code:: python def resolve_foo(parent, info, **kwargs): Where "foo" is the name of the field declared in the ``Query`` object. .. code:: python import graphene from .models import Question from .types import QuestionType class Query(graphene.ObjectType): foo = graphene.List(QuestionType) def resolve_foo(root, info, **kwargs): id = kwargs.get("id") return Question.objects.get(id) Arguments ~~~~~~~~~ Additionally, Resolvers will receive **any arguments declared in the field definition**. This allows you to provide input arguments in your GraphQL server and can be useful for custom queries. .. code:: python import graphene from .models import Question from .types import QuestionType class Query(graphene.ObjectType): question = graphene.Field( QuestionType, foo=graphene.String(), bar=graphene.Int() ) def resolve_question(root, info, foo=None, bar=None): # If `foo` or `bar` are declared in the GraphQL query they will be here, else None. return Question.objects.filter(foo=foo, bar=bar).first() Info ~~~~ The ``info`` argument passed to all resolve methods holds some useful information. For Graphene-Django, the ``info.context`` attribute is the ``HTTPRequest`` object that would be familiar to any Django developer. This gives you the full functionality of Django's ``HTTPRequest`` in your resolve methods, such as checking for authenticated users: .. code:: python import graphene from .models import Question from .types import QuestionType class Query(graphene.ObjectType): questions = graphene.List(QuestionType) def resolve_questions(root, info): # See if a user is authenticated if info.context.user.is_authenticated(): return Question.objects.all() else: return Question.objects.none() DjangoObjectTypes ~~~~~~~~~~~~~~~~~ A Resolver that maps to a defined `DjangoObjectType` should only use methods that return a queryset. Queryset methods like `values` will return dictionaries, use `defer` instead. Plain ObjectTypes ----------------- With Graphene-Django you are not limited to just Django Models - you can use the standard ``ObjectType`` to create custom fields or to provide an abstraction between your internal Django models and your external API. .. code:: python import graphene from .models import Question class MyQuestion(graphene.ObjectType): text = graphene.String() class Query(graphene.ObjectType): question = graphene.Field(MyQuestion, question_id=graphene.String()) def resolve_question(root, info, question_id): question = Question.objects.get(pk=question_id) return MyQuestion( text=question.question_text ) For more information and more examples, please see the `core object type documentation `__. Relay ----- `Relay `__ with Graphene-Django gives us some additional features: - Pagination and slicing. - An abstract ``id`` value which contains enough info for the server to know its type and its id. There is one additional import and a single line of code needed to adopt this: Full example ~~~~~~~~~~~~ See the `Relay documentation `__ on the core graphene pages for more information on customizing the Relay experience. .. code:: python from graphene import relay from graphene_django import DjangoObjectType from .models import Question class QuestionType(DjangoObjectType): class Meta: model = Question interfaces = (relay.Node,) # make sure you add this fields = "__all__" class QuestionConnection(relay.Connection): class Meta: node = QuestionType class Query: questions = relay.ConnectionField(QuestionConnection) def resolve_questions(root, info, **kwargs): return Question.objects.all() You can now execute queries like: .. code:: graphql { questions (first: 2, after: "YXJyYXljb25uZWN0aW9uOjEwNQ==") { pageInfo { startCursor endCursor hasNextPage hasPreviousPage } edges { cursor node { id question_text } } } } Which returns: .. code:: json { "data": { "questions": { "pageInfo": { "startCursor": "YXJyYXljb25uZWN0aW9uOjEwNg==", "endCursor": "YXJyYXljb25uZWN0aW9uOjEwNw==", "hasNextPage": true, "hasPreviousPage": false }, "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjEwNg==", "node": { "id": "UGxhY2VUeXBlOjEwNw==", "question_text": "How did we get here?" } }, { "cursor": "YXJyYXljb25uZWN0aW9uOjEwNw==", "node": { "id": "UGxhY2VUeXBlOjEwOA==", "name": "Where are we?" } } ] } } } Note that relay implements :code:`pagination` capabilities automatically, adding a :code:`pageInfo` element, and including :code:`cursor` on nodes. These elements are included in the above example for illustration. To learn more about Pagination in general, take a look at `Pagination `__ on the GraphQL community site. django-graphene-3.2.3/docs/requirements.txt000066400000000000000000000002151476451357100207770ustar00rootroot00000000000000Sphinx==7.0.0 sphinx-autobuild==2021.3.14 pygments-graphql-lexer==0.1.0 # Docs template http://graphene-python.org/sphinx_graphene_theme.zip django-graphene-3.2.3/docs/schema.py000066400000000000000000000026321476451357100173320ustar00rootroot00000000000000import graphene from graphene_django.types import DjangoObjectType from cookbook.ingredients.models import Category, Ingredient class CategoryType(DjangoObjectType): class Meta: model = Category fields = "__all__" class IngredientType(DjangoObjectType): class Meta: model = Ingredient fields = "__all__" class Query: category = graphene.Field(CategoryType, id=graphene.Int(), name=graphene.String()) all_categories = graphene.List(CategoryType) ingredient = graphene.Field( IngredientType, id=graphene.Int(), name=graphene.String() ) all_ingredients = graphene.List(IngredientType) def resolve_all_categories(self, info, **kwargs): return Category.objects.all() def resolve_all_ingredients(self, info, **kwargs): return Ingredient.objects.all() def resolve_category(self, info, **kwargs): id = kwargs.get("id") name = kwargs.get("name") if id is not None: return Category.objects.get(pk=id) if name is not None: return Category.objects.get(name=name) return None def resolve_ingredient(self, info, **kwargs): id = kwargs.get("id") name = kwargs.get("name") if id is not None: return Ingredient.objects.get(pk=id) if name is not None: return Ingredient.objects.get(name=name) return None django-graphene-3.2.3/docs/schema.rst000066400000000000000000000024111476451357100175050ustar00rootroot00000000000000Schema ====== 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: .. code:: python import graphene class Query(graphene.ObjectType): pass class Mutation(graphene.ObjectType): pass schema = graphene.Schema(query=Query, mutation=Mutation) This schema doesn't do anything yet, but it is ready to accept new Query or Mutation fields. Adding to the schema -------------------- If you have defined a ``Query`` or ``Mutation``, you can register them with the schema: .. code:: python import graphene import my_app.schema.Query import my_app.schema.Mutation class Query( my_app.schema.Query, # Add your Query objects here graphene.ObjectType ): pass class Mutation( my_app.schema.Mutation, # Add your Mutation objects here graphene.ObjectType ): pass schema = graphene.Schema(query=Query, mutation=Mutation) You can add as many mixins to the base ``Query`` and ``Mutation`` objects as you like. Read more about Schema on the `core graphene docs `__django-graphene-3.2.3/docs/settings.rst000066400000000000000000000150331476451357100201110ustar00rootroot00000000000000Settings ======== Graphene-Django can be customised using settings. This page explains each setting and their defaults. Usage ----- Add settings to your Django project by creating a Dictionary with name ``GRAPHENE`` in the project's ``settings.py``: .. code:: python GRAPHENE = { ... } ``SCHEMA`` ---------- The location of the top-level ``Schema`` class. Default: ``None`` .. code:: python GRAPHENE = { 'SCHEMA': 'path.to.schema.schema', } ``SCHEMA_OUTPUT`` ----------------- The name of the file where the GraphQL schema output will go. Default: ``schema.json`` .. code:: python GRAPHENE = { 'SCHEMA_OUTPUT': 'schema.json', } ``SCHEMA_INDENT`` ----------------- The indentation level of the schema output. Default: ``2`` .. code:: python GRAPHENE = { 'SCHEMA_INDENT': 2, } ``MIDDLEWARE`` -------------- A tuple of middleware that will be executed for each GraphQL query. See the `middleware documentation `__ for more information. Default: ``()`` .. code:: python GRAPHENE = { 'MIDDLEWARE': ( 'path.to.my.middleware.class', ), } ``RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST`` ------------------------------------------ Enforces relay queries to have the ``first`` or ``last`` argument. Default: ``False`` .. code:: python GRAPHENE = { 'RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST': False, } ``RELAY_CONNECTION_MAX_LIMIT`` ------------------------------ The maximum size of objects that can be requested through a relay connection. Default: ``100`` .. code:: python GRAPHENE = { 'RELAY_CONNECTION_MAX_LIMIT': 100, } ``CAMELCASE_ERRORS`` -------------------- When set to ``True`` field names in the ``errors`` object will be camel case. By default they will be snake case. Default: ``False`` .. code:: python GRAPHENE = { 'CAMELCASE_ERRORS': False, } # result = schema.execute(...) print(result.errors) # [ # { # 'field': 'test_field', # 'messages': ['This field is required.'], # } # ] .. code:: python GRAPHENE = { 'CAMELCASE_ERRORS': True, } # result = schema.execute(...) print(result.errors) # [ # { # 'field': 'testField', # 'messages': ['This field is required.'], # } # ] ``DJANGO_CHOICE_FIELD_ENUM_CONVERT`` -------------------------------------- When set to ``True`` Django choice fields are automatically converted into Enum types. Can be disabled globally by setting it to ``False``. Default: ``True`` ``DJANGO_CHOICE_FIELD_ENUM_V2_NAMING`` -------------------------------------- Set to ``True`` to use the old naming format for the auto generated Enum types from Django choice fields. The old format looks like this: ``{object_name}_{field_name}`` Default: ``False`` ``DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME`` ---------------------------------------- Define the path of a function that takes the Django choice field and returns a string to completely customise the naming for the Enum type. If set to a function then the ``DJANGO_CHOICE_FIELD_ENUM_V2_NAMING`` setting is ignored. Default: ``None`` .. code:: python # myapp.utils def enum_naming(field): if isinstance(field.model, User): return f"CustomUserEnum{field.name.title()}" return f"CustomEnum{field.name.title()}" GRAPHENE = { 'DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME': "myapp.utils.enum_naming" } ``SUBSCRIPTION_PATH`` --------------------- Define an alternative URL path where subscription operations should be routed. The GraphiQL interface will use this setting to intelligently route subscription operations. This is useful if you have more advanced infrastructure requirements that prevent websockets from being handled at the same path (e.g., a WSGI server listening at ``/graphql`` and an ASGI server listening at ``/ws/graphql``). Default: ``None`` .. code:: python GRAPHENE = { 'SUBSCRIPTION_PATH': "/ws/graphql" } ``GRAPHIQL_HEADER_EDITOR_ENABLED`` ---------------------------------- GraphiQL starting from version 1.0.0 allows setting custom headers in similar fashion to query variables. Set to ``False`` if you want to disable GraphiQL headers editor tab for some reason. This setting is passed to ``headerEditorEnabled`` GraphiQL options, for details refer to GraphiQLDocs_. Default: ``True`` .. code:: python GRAPHENE = { 'GRAPHIQL_HEADER_EDITOR_ENABLED': True, } ``TESTING_ENDPOINT`` -------------------- Define the graphql endpoint url used for the `GraphQLTestCase` class. Default: ``/graphql`` .. code:: python GRAPHENE = { 'TESTING_ENDPOINT': '/customEndpoint' } ``GRAPHIQL_SHOULD_PERSIST_HEADERS`` ----------------------------------- Set to ``True`` if you want to persist GraphiQL headers after refreshing the page. This setting is passed to ``shouldPersistHeaders`` GraphiQL options, for details refer to GraphiQLDocs_. Default: ``False`` .. code:: python GRAPHENE = { 'GRAPHIQL_SHOULD_PERSIST_HEADERS': False, } ``GRAPHIQL_INPUT_VALUE_DEPRECATION`` ------------------------------------ Set to ``True`` if you want GraphiQL to show any deprecated fields on input object types' docs. For example, having this schema: .. code:: python class MyMutationInputType(graphene.InputObjectType): old_field = graphene.String(deprecation_reason="You should now use 'newField' instead.") new_field = graphene.String() class MyMutation(graphene.Mutation): class Arguments: input = types.MyMutationInputType() GraphiQL will add a ``Show Deprecated Fields`` button to toggle information display on ``oldField`` and its deprecation reason. Otherwise, you would get neither a button nor any information at all on ``oldField``. This setting is passed to ``inputValueDeprecation`` GraphiQL options, for details refer to GraphiQLDocs_. Default: ``False`` .. code:: python GRAPHENE = { 'GRAPHIQL_INPUT_VALUE_DEPRECATION': False, } .. _GraphiQLDocs: https://graphiql-test.netlify.app/typedoc/modules/graphiql_react#graphiqlprovider-2 ``MAX_VALIDATION_ERRORS`` ------------------------------------ In case ``validation_rules`` are provided to ``GraphQLView``, if this is set to a non-negative ``int`` value, ``graphql.validation.validate`` will stop validation after this number of errors has been reached. If not set or set to ``None``, the maximum number of errors will follow ``graphql.validation.validate`` default *i.e.* 100. Default: ``None`` django-graphene-3.2.3/docs/subscriptions.rst000066400000000000000000000040421476451357100211560ustar00rootroot00000000000000Subscriptions ============= The ``graphene-django`` project does not currently support GraphQL subscriptions out of the box. However, there are several community-driven modules for adding subscription support, and the provided GraphiQL interface supports running subscription operations over a websocket. To implement websocket-based support for GraphQL subscriptions, you’ll need to do the following: 1. Install and configure `django-channels `_. 2. Install and configure* a third-party module for adding subscription support over websockets. A few options include: - `graphql-python/graphql-ws `_ - `datavance/django-channels-graphql-ws `_ - `jaydenwindle/graphene-subscriptions `_ 3. Ensure that your application (or at least your GraphQL endpoint) is being served via an ASGI protocol server like daphne (built in to ``django-channels``), `uvicorn `_, or `hypercorn `_. .. *** Note:** By default, the GraphiQL interface that comes with ``graphene-django`` assumes that you are handling subscriptions at the same path as any other operation (i.e., you configured both ``urls.py`` and ``routing.py`` to handle GraphQL operations at the same path, like ``/graphql``). If these URLs differ, GraphiQL will try to run your subscription over HTTP, which will produce an error. If you need to use a different URL for handling websocket connections, you can configure ``SUBSCRIPTION_PATH`` in your ``settings.py``: .. code:: python GRAPHENE = { # ... "SUBSCRIPTION_PATH": "/ws/graphql" # The path you configured in `routing.py`, including a leading slash. } Once your application is properly configured to handle subscriptions, you can use the GraphiQL interface to test subscriptions like any other operation. django-graphene-3.2.3/docs/testing.rst000066400000000000000000000105101476451357100177210ustar00rootroot00000000000000Testing API calls with django ============================= Using unittest -------------- If you want to unittest your API calls derive your test case from the class `GraphQLTestCase`. The default endpoint for testing is `/graphql`. You can override this in the `settings `__. Usage: .. code:: python import json from graphene_django.utils.testing import GraphQLTestCase class MyFancyTestCase(GraphQLTestCase): def test_some_query(self): response = self.query( ''' query { myModel { id name } } ''', operation_name='myModel' ) content = json.loads(response.content) # This validates the status code and if you get errors self.assertResponseNoErrors(response) # Add some more asserts if you like ... def test_query_with_variables(self): response = self.query( ''' query myModel($id: Int!){ myModel(id: $id) { id name } } ''', operation_name='myModel', variables={'id': 1} ) content = json.loads(response.content) # This validates the status code and if you get errors self.assertResponseNoErrors(response) # Add some more asserts if you like ... def test_some_mutation(self): response = self.query( ''' mutation myMutation($input: MyMutationInput!) { myMutation(input: $input) { my-model { id name } } } ''', operation_name='myMutation', input_data={'my_field': 'foo', 'other_field': 'bar'} ) # This validates the status code and if you get errors self.assertResponseNoErrors(response) # Add some more asserts if you like ... For testing mutations that are executed within a transaction you should subclass `GraphQLTransactionTestCase` Usage: .. code:: python import json from graphene_django.utils.testing import GraphQLTransactionTestCase class MyFancyTransactionTestCase(GraphQLTransactionTestCase): def test_some_mutation_that_executes_within_a_transaction(self): response = self.query( ''' mutation myMutation($input: MyMutationInput!) { myMutation(input: $input) { my-model { id name } } } ''', operation_name='myMutation', input_data={'my_field': 'foo', 'other_field': 'bar'} ) # This validates the status code and if you get errors self.assertResponseNoErrors(response) # Add some more asserts if you like ... Using pytest ------------ To use pytest define a simple fixture using the query helper below .. code:: python # Create a fixture using the graphql_query helper and `client` fixture from `pytest-django`. import json import pytest from graphene_django.utils.testing import graphql_query @pytest.fixture def client_query(client): def func(*args, **kwargs): return graphql_query(*args, **kwargs, client=client) return func # Test you query using the client_query fixture def test_some_query(client_query): response = client_query( ''' query { myModel { id name } } ''', operation_name='myModel' ) content = json.loads(response.content) assert 'errors' not in content django-graphene-3.2.3/docs/tutorial-plain.rst000066400000000000000000000245711476451357100212240ustar00rootroot00000000000000Basic Tutorial =========================================== Graphene Django has a number of additional features that are designed to make working with Django easy. Our primary focus in this tutorial is to give a good understanding of how to connect models from Django ORM to Graphene object types. Set up the Django project ------------------------- We will set up the project, create the following: - A Django project called ``cookbook`` - An app within ``cookbook`` called ``ingredients`` .. code:: bash # Create the project directory mkdir cookbook cd cookbook # Create a virtualenv to isolate our package dependencies locally virtualenv env source env/bin/activate # On Windows use `env\Scripts\activate` # Install Django and Graphene with Django support pip install django graphene_django # Set up a new project with a single application django-admin startproject cookbook . # Note the trailing '.' character cd cookbook django-admin startapp ingredients Now sync your database for the first time: .. code:: bash cd .. python manage.py migrate Let's create a few simple models... Defining our models ^^^^^^^^^^^^^^^^^^^ Let's get started with these models: .. code:: python # cookbook/ingredients/models.py from django.db import models class Category(models.Model): name = models.CharField(max_length=100) def __str__(self): return self.name class Ingredient(models.Model): name = models.CharField(max_length=100) notes = models.TextField() category = models.ForeignKey( Category, related_name="ingredients", on_delete=models.CASCADE ) def __str__(self): return self.name Add ingredients as INSTALLED_APPS: .. code:: python # cookbook/settings.py INSTALLED_APPS = [ ... # Install the ingredients app "cookbook.ingredients", ] Make sure the app name in ``cookbook.ingredients.apps.IngredientsConfig`` is set to ``cookbook.ingredients``. .. code:: python # cookbook/ingredients/apps.py from django.apps import AppConfig class IngredientsConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'cookbook.ingredients' Don't forget to create & run migrations: .. code:: bash python manage.py makemigrations python manage.py migrate Load some test data ^^^^^^^^^^^^^^^^^^^ Now is a good time to load up some test data. The easiest option will be to `download the ingredients.json `__ fixture and place it in ``cookbook/ingredients/fixtures/ingredients.json``. You can then run the following: .. code:: bash python manage.py loaddata ingredients Installed 6 object(s) from 1 fixture(s) Alternatively you can use the Django admin interface to create some data yourself. You'll need to run the development server (see below), and create a login for yourself too (``python manage.py createsuperuser``). Register models with admin panel: .. code:: python # cookbook/ingredients/admin.py from django.contrib import admin from cookbook.ingredients.models import Category, Ingredient admin.site.register(Category) admin.site.register(Ingredient) Hello GraphQL - Schema and Object Types --------------------------------------- In order to make queries to our Django project, we are going to need few things: * Schema with defined object types * A view, taking queries as input and returning the result GraphQL presents your objects to the world as a graph structure rather than a more hierarchical structure to which you may be accustomed. In order to create this representation, Graphene needs to know about each *type* of object which will appear in the graph. This graph also has a *root type* through which all access begins. This is the ``Query`` class below. To create GraphQL types for each of our Django models, we are going to subclass the ``DjangoObjectType`` class which will automatically define GraphQL fields that correspond to the fields on the Django models. After we've done that, we will list those types as fields in the ``Query`` class. Create ``cookbook/schema.py`` and type the following: .. code:: python # cookbook/schema.py import graphene from graphene_django import DjangoObjectType from cookbook.ingredients.models import Category, Ingredient class CategoryType(DjangoObjectType): class Meta: model = Category fields = ("id", "name", "ingredients") class IngredientType(DjangoObjectType): class Meta: model = Ingredient fields = ("id", "name", "notes", "category") class Query(graphene.ObjectType): all_ingredients = graphene.List(IngredientType) category_by_name = graphene.Field(CategoryType, name=graphene.String(required=True)) def resolve_all_ingredients(root, info): # We can easily optimize query count in the resolve method return Ingredient.objects.select_related("category").all() def resolve_category_by_name(root, info, name): try: return Category.objects.get(name=name) except Category.DoesNotExist: return None schema = graphene.Schema(query=Query) You can think of this as being something like your top-level ``urls.py`` file. Testing everything so far ------------------------- We are going to do some configuration work, in order to have a working Django where we can test queries, before we move on, updating our schema. Update settings ^^^^^^^^^^^^^^^ Next, install your app and GraphiQL in your Django project. GraphiQL is a web-based integrated development environment to assist in the writing and executing of GraphQL queries. It will provide us with a simple and easy way of testing our cookbook project. Add ``graphene_django`` to ``INSTALLED_APPS`` in ``cookbook/settings.py``: .. code:: python # cookbook/settings.py INSTALLED_APPS = [ ... "graphene_django", ] And then add the ``SCHEMA`` to the ``GRAPHENE`` config in ``cookbook/settings.py``: .. code:: python # cookbook/settings.py GRAPHENE = { "SCHEMA": "cookbook.schema.schema" } Alternatively, we can specify the schema to be used in the urls definition, as explained below. Creating GraphQL and GraphiQL views ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Unlike a RESTful API, there is only a single URL from which GraphQL is accessed. Requests to this URL are handled by Graphene's ``GraphQLView`` view. This view will serve as GraphQL endpoint. As we want to have the aforementioned GraphiQL we specify that on the parameters with ``graphiql=True``. .. code:: python # cookbook/urls.py from django.contrib import admin from django.urls import path from django.views.decorators.csrf import csrf_exempt from graphene_django.views import GraphQLView urlpatterns = [ path("admin/", admin.site.urls), path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True))), ] If we didn't specify the target schema in the Django settings file as explained above, we can do so here using: .. code:: python # cookbook/urls.py from django.contrib import admin from django.urls import path from django.views.decorators.csrf import csrf_exempt from graphene_django.views import GraphQLView from cookbook.schema import schema urlpatterns = [ path("admin/", admin.site.urls), path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True, schema=schema))), ] Testing our GraphQL schema ^^^^^^^^^^^^^^^^^^^^^^^^^^ We're now ready to test the API we've built. Let's fire up the server from the command line. .. code:: bash python manage.py runserver Performing system checks... Django version 3.0.7, using settings 'cookbook.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C. Go to `localhost:8000/graphql `__ and type your first query! .. code:: query { allIngredients { id name } } If you are using the provided fixtures, you will see the following response: .. code:: { "data": { "allIngredients": [ { "id": "1", "name": "Eggs" }, { "id": "2", "name": "Milk" }, { "id": "3", "name": "Beef" }, { "id": "4", "name": "Chicken" } ] } } Congratulations, you have created a working GraphQL server 🥳! Note: Graphene `automatically camelcases `__ all field names for better compatibility with JavaScript clients. Getting relations ----------------- Using the current schema we can query for relations too. This is where GraphQL becomes really powerful! For example, we may want to get a specific categories and list all ingredients that are in that category. We can do that with the following query: .. code:: query { categoryByName(name: "Dairy") { id name ingredients { id name } } } This will give you (in case you are using the fixtures) the following result: .. code:: { "data": { "categoryByName": { "id": "1", "name": "Dairy", "ingredients": [ { "id": "1", "name": "Eggs" }, { "id": "2", "name": "Milk" } ] } } } We can also list all ingredients and get information for the category they are in: .. code:: query { allIngredients { id name category { id name } } } Summary ------- As you can see, GraphQL is very powerful and integrating Django models allows you to get started with a working server quickly. If you want to put things like ``django-filter`` and automatic pagination in action, you should continue with the :ref:`Relay tutorial`. A good idea is to check the `Graphene `__ documentation so that you are familiar with it as well. django-graphene-3.2.3/docs/tutorial-relay.rst000066400000000000000000000233361476451357100212330ustar00rootroot00000000000000.. _Relay tutorial: Relay tutorial ======================================== Graphene has a number of additional features that are designed to make working with Django *really simple*. Note: The code in this quickstart is pulled from the `cookbook example app `__. A good idea is to check the following things first: * `Graphene Relay documentation `__ * `GraphQL Relay Specification `__ Setup the Django project ------------------------ We will setup the project, create the following: - A Django project called ``cookbook`` - An app within ``cookbook`` called ``ingredients`` .. code:: bash # Create the project directory mkdir cookbook cd cookbook # Create a virtualenv to isolate our package dependencies locally virtualenv env source env/bin/activate # On Windows use `env\Scripts\activate` # Install Django and Graphene with Django support pip install django pip install graphene_django # Set up a new project with a single application django-admin.py startproject cookbook . # Note the trailing '.' character cd cookbook django-admin.py startapp ingredients Now sync your database for the first time: .. code:: bash python manage.py migrate Let's create a few simple models... Defining our models ^^^^^^^^^^^^^^^^^^^ Let's get started with these models: .. code:: python # cookbook/ingredients/models.py from django.db import models class Category(models.Model): name = models.CharField(max_length=100) def __str__(self): return self.name class Ingredient(models.Model): name = models.CharField(max_length=100) notes = models.TextField() category = models.ForeignKey(Category, related_name='ingredients', on_delete=models.CASCADE) def __str__(self): return self.name Don't forget to create & run migrations: .. code:: bash python manage.py makemigrations python manage.py migrate Load some test data ^^^^^^^^^^^^^^^^^^^ Now is a good time to load up some test data. The easiest option will be to `download the ingredients.json `__ fixture and place it in ``cookbook/ingredients/fixtures/ingredients.json``. You can then run the following: .. code:: bash $ python ./manage.py loaddata ingredients Installed 6 object(s) from 1 fixture(s) Alternatively you can use the Django admin interface to create some data yourself. You'll need to run the development server (see below), and create a login for yourself too (``./manage.py createsuperuser``). Schema ------ GraphQL presents your objects to the world as a graph structure rather than a more hierarchical structure to which you may be accustomed. In order to create this representation, Graphene needs to know about each *type* of object which will appear in the graph. This graph also has a *root type* through which all access begins. This is the ``Query`` class below. In this example, we provide the ability to list all ingredients via ``all_ingredients``, and the ability to obtain a specific ingredient via ``ingredient``. Create ``cookbook/ingredients/schema.py`` and type the following: .. code:: python # cookbook/ingredients/schema.py from graphene import relay, ObjectType from graphene_django import DjangoObjectType from graphene_django.filter import DjangoFilterConnectionField from ingredients.models import Category, Ingredient # Graphene will automatically map the Category model's fields onto the CategoryNode. # This is configured in the CategoryNode's Meta class (as you can see below) class CategoryNode(DjangoObjectType): class Meta: model = Category fields = '__all__' filter_fields = ['name', 'ingredients'] interfaces = (relay.Node, ) class IngredientNode(DjangoObjectType): class Meta: model = Ingredient fields = '__all__' # Allow for some more advanced filtering here filter_fields = { 'name': ['exact', 'icontains', 'istartswith'], 'notes': ['exact', 'icontains'], 'category': ['exact'], 'category__name': ['exact'], } interfaces = (relay.Node, ) class Query(ObjectType): category = relay.Node.Field(CategoryNode) all_categories = DjangoFilterConnectionField(CategoryNode) ingredient = relay.Node.Field(IngredientNode) all_ingredients = DjangoFilterConnectionField(IngredientNode) The filtering functionality is provided by `django-filter `__. See the `usage documentation `__ for details on the format for ``filter_fields``. While optional, this tutorial makes use of this functionality so you will need to install ``django-filter`` for this tutorial to work: .. code:: bash pip install django-filter Note that the above ``Query`` class is marked as 'abstract'. This is because we will now create a project-level query which will combine all our app-level queries. Create the parent project-level ``cookbook/schema.py``: .. code:: python import graphene import ingredients.schema class Query(ingredients.schema.Query, graphene.ObjectType): # This class will inherit from multiple Queries # as we begin to add more apps to our project pass schema = graphene.Schema(query=Query) You can think of this as being something like your top-level ``urls.py`` file (although it currently lacks any namespacing). Testing everything so far ------------------------- Update settings ^^^^^^^^^^^^^^^ Next, install your app and GraphiQL in your Django project. GraphiQL is a web-based integrated development environment to assist in the writing and executing of GraphQL queries. It will provide us with a simple and easy way of testing our cookbook project. Add ``ingredients`` and ``graphene_django`` to ``INSTALLED_APPS`` in ``cookbook/settings.py``: .. code:: python INSTALLED_APPS = [ ... # This will also make the `graphql_schema` management command available 'graphene_django', # Install the ingredients app 'ingredients', ] And then add the ``SCHEMA`` to the ``GRAPHENE`` config in ``cookbook/settings.py``: .. code:: python GRAPHENE = { 'SCHEMA': 'cookbook.schema.schema' } Alternatively, we can specify the schema to be used in the urls definition, as explained below. Creating GraphQL and GraphiQL views ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Unlike a RESTful API, there is only a single URL from which GraphQL is accessed. Requests to this URL are handled by Graphene's ``GraphQLView`` view. This view will serve as GraphQL endpoint. As we want to have the aforementioned GraphiQL we specify that on the params with ``graphiql=True``. .. code:: python from django.conf.urls import url, include from django.contrib import admin from graphene_django.views import GraphQLView urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^graphql$', GraphQLView.as_view(graphiql=True)), ] If we didn't specify the target schema in the Django settings file as explained above, we can do so here using: .. code:: python from django.conf.urls import url, include from django.contrib import admin from graphene_django.views import GraphQLView from cookbook.schema import schema urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^graphql$', GraphQLView.as_view(graphiql=True, schema=schema)), ] Testing our GraphQL schema ^^^^^^^^^^^^^^^^^^^^^^^^^^ We're now ready to test the API we've built. Let's fire up the server from the command line. .. code:: bash $ python ./manage.py runserver Performing system checks... Django version 3.1.7, using settings 'cookbook.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C. Go to `localhost:8000/graphql `__ and type your first query! .. code:: query { allIngredients { edges { node { id, name } } } } The above will return the names & IDs for all ingredients. But perhaps you want a specific ingredient: .. code:: query { # Graphene creates globally unique IDs for all objects. # You may need to copy this value from the results of the first query ingredient(id: "SW5ncmVkaWVudE5vZGU6MQ==") { name } } You can also get each ingredient for each category: .. code:: query { allCategories { edges { node { name, ingredients { edges { node { name } } } } } } } Or you can get only 'meat' ingredients containing the letter 'e': .. code:: query { # You can also use `category: "CATEGORY GLOBAL ID"` allIngredients(name_Icontains: "e", category_Name: "Meat") { edges { node { name } } } } Final Steps ^^^^^^^^^^^ We have created a GraphQL endpoint that will work with Relay, but for Relay to work it needs access to a (non python) schema. Instructions to export the schema can be found on the `Introspection Schema `__ part of this guide. django-graphene-3.2.3/docs/validation.rst000066400000000000000000000016271476451357100204070ustar00rootroot00000000000000Query Validation ================ 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``. .. code:: python from django.urls import path from graphene.validation import DisableIntrospection from graphene_django.views import GraphQLView urlpatterns = [ path("graphql", GraphQLView.as_view(validation_rules=(DisableIntrospection,))), ] or .. code:: python from django.urls import path from graphene.validation import DisableIntrospection from graphene_django.views import GraphQLView class View(GraphQLView): validation_rules = (DisableIntrospection,) urlpatterns = [ path("graphql", View.as_view()), ] django-graphene-3.2.3/examples/000077500000000000000000000000001476451357100164035ustar00rootroot00000000000000django-graphene-3.2.3/examples/__init__.py000066400000000000000000000000001476451357100205020ustar00rootroot00000000000000django-graphene-3.2.3/examples/cookbook-plain/000077500000000000000000000000001476451357100213125ustar00rootroot00000000000000django-graphene-3.2.3/examples/cookbook-plain/README.md000066400000000000000000000035651476451357100226020ustar00rootroot00000000000000Cookbook Example Django Project =============================== This example project demos integration between Graphene and Django. The project contains two apps, one named `ingredients` and another named `recipes`. Getting started --------------- First you'll need to get the source of the project. Do this by cloning the whole Graphene repository: ```bash # Get the example project code git clone https://github.com/graphql-python/graphene-django.git cd graphene-django/examples/cookbook-plain ``` It is good idea (but not required) to create a virtual environment for this project. We'll do this using [virtualenv](http://docs.python-guide.org/en/latest/dev/virtualenvs/) to keep things simple, but you may also find something like [virtualenvwrapper](https://virtualenvwrapper.readthedocs.org/en/latest/) to be useful: ```bash # Create a virtualenv in which we can install the dependencies virtualenv env source env/bin/activate ``` Now we can install our dependencies: ```bash pip install -r requirements.txt ``` Now setup our database: ```bash # Setup the database ./manage.py migrate # Load some example data ./manage.py loaddata ingredients # Create an admin user (useful for logging into the admin UI # at http://127.0.0.1:8000/admin) ./manage.py createsuperuser ``` Now you should be ready to start the server: ```bash ./manage.py runserver ``` Now head on over to [http://127.0.0.1:8000/graphql](http://127.0.0.1:8000/graphql) and run some queries! (See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial-plain/#testing-our-graphql-schema) for some example queries) Testing local graphene-django changes ------------------------------------- In `requirements.txt`, replace the entire `graphene-django=...` line with the following (so that we install the local version instead of the one from PyPI): ``` ../../ # graphene-django ``` django-graphene-3.2.3/examples/cookbook-plain/__init__.py000066400000000000000000000000001476451357100234110ustar00rootroot00000000000000django-graphene-3.2.3/examples/cookbook-plain/cookbook/000077500000000000000000000000001476451357100231205ustar00rootroot00000000000000django-graphene-3.2.3/examples/cookbook-plain/cookbook/__init__.py000066400000000000000000000000001476451357100252170ustar00rootroot00000000000000django-graphene-3.2.3/examples/cookbook-plain/cookbook/ingredients/000077500000000000000000000000001476451357100254335ustar00rootroot00000000000000django-graphene-3.2.3/examples/cookbook-plain/cookbook/ingredients/__init__.py000066400000000000000000000000001476451357100275320ustar00rootroot00000000000000django-graphene-3.2.3/examples/cookbook-plain/cookbook/ingredients/admin.py000066400000000000000000000004351476451357100270770ustar00rootroot00000000000000from django.contrib import admin from cookbook.ingredients.models import Category, Ingredient @admin.register(Ingredient) class IngredientAdmin(admin.ModelAdmin): list_display = ("id", "name", "category") list_editable = ("name", "category") admin.site.register(Category) django-graphene-3.2.3/examples/cookbook-plain/cookbook/ingredients/apps.py000066400000000000000000000002451476451357100267510ustar00rootroot00000000000000from django.apps import AppConfig class IngredientsConfig(AppConfig): name = "cookbook.ingredients" label = "ingredients" verbose_name = "Ingredients" django-graphene-3.2.3/examples/cookbook-plain/cookbook/ingredients/fixtures/000077500000000000000000000000001476451357100273045ustar00rootroot00000000000000django-graphene-3.2.3/examples/cookbook-plain/cookbook/ingredients/fixtures/ingredients.json000066400000000000000000000015671476451357100325230ustar00rootroot00000000000000[ { "fields": { "name": "Dairy" }, "model": "ingredients.category", "pk": 1 }, { "fields": { "name": "Meat" }, "model": "ingredients.category", "pk": 2 }, { "fields": { "category": 1, "name": "Eggs", "notes": "Good old eggs" }, "model": "ingredients.ingredient", "pk": 1 }, { "fields": { "category": 1, "name": "Milk", "notes": "Comes from a cow" }, "model": "ingredients.ingredient", "pk": 2 }, { "fields": { "category": 2, "name": "Beef", "notes": "Much like milk, this comes from a cow" }, "model": "ingredients.ingredient", "pk": 3 }, { "fields": { "category": 2, "name": "Chicken", "notes": "Definitely doesn't come from a cow" }, "model": "ingredients.ingredient", "pk": 4 } ] django-graphene-3.2.3/examples/cookbook-plain/cookbook/ingredients/migrations/000077500000000000000000000000001476451357100276075ustar00rootroot00000000000000django-graphene-3.2.3/examples/cookbook-plain/cookbook/ingredients/migrations/0001_initial.py000066400000000000000000000027401476451357100322550ustar00rootroot00000000000000# Generated by Django 1.9 on 2015-12-04 18:15 import django.db.models.deletion from django.db import migrations, models class Migration(migrations.Migration): initial = True dependencies = [] operations = [ migrations.CreateModel( name="Category", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("name", models.CharField(max_length=100)), ], ), migrations.CreateModel( name="Ingredient", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("name", models.CharField(max_length=100)), ("notes", models.TextField()), ( "category", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="ingredients", to="ingredients.Category", ), ), ], ), ] 0002_auto_20161104_0050.py000066400000000000000000000006031476451357100331340ustar00rootroot00000000000000django-graphene-3.2.3/examples/cookbook-plain/cookbook/ingredients/migrations# Generated by Django 1.9 on 2016-11-04 00:50 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("ingredients", "0001_initial"), ] operations = [ migrations.AlterField( model_name="ingredient", name="notes", field=models.TextField(blank=True, null=True), ), ] 0003_auto_20181018_1746.py000066400000000000000000000005531476451357100331640ustar00rootroot00000000000000django-graphene-3.2.3/examples/cookbook-plain/cookbook/ingredients/migrations# Generated by Django 2.0 on 2018-10-18 17:46 from django.db import migrations class Migration(migrations.Migration): dependencies = [ ("ingredients", "0002_auto_20161104_0050"), ] operations = [ migrations.AlterModelOptions( name="category", options={"verbose_name_plural": "Categories"}, ), ] django-graphene-3.2.3/examples/cookbook-plain/cookbook/ingredients/migrations/__init__.py000066400000000000000000000000001476451357100317060ustar00rootroot00000000000000django-graphene-3.2.3/examples/cookbook-plain/cookbook/ingredients/models.py000066400000000000000000000007701476451357100272740ustar00rootroot00000000000000from django.db import models class Category(models.Model): class Meta: verbose_name_plural = "Categories" name = models.CharField(max_length=100) def __str__(self): return self.name class Ingredient(models.Model): name = models.CharField(max_length=100) notes = models.TextField(null=True, blank=True) category = models.ForeignKey( Category, related_name="ingredients", on_delete=models.CASCADE ) def __str__(self): return self.name django-graphene-3.2.3/examples/cookbook-plain/cookbook/ingredients/schema.py000066400000000000000000000025551476451357100272540ustar00rootroot00000000000000import graphene from graphene_django.types import DjangoObjectType from .models import Category, Ingredient class CategoryType(DjangoObjectType): class Meta: model = Category fields = "__all__" class IngredientType(DjangoObjectType): class Meta: model = Ingredient fields = "__all__" class Query: category = graphene.Field(CategoryType, id=graphene.Int(), name=graphene.String()) all_categories = graphene.List(CategoryType) ingredient = graphene.Field( IngredientType, id=graphene.Int(), name=graphene.String() ) all_ingredients = graphene.List(IngredientType) def resolve_all_categories(self, context): return Category.objects.all() def resolve_all_ingredients(self, context): # We can easily optimize query count in the resolve method return Ingredient.objects.select_related("category").all() def resolve_category(self, context, id=None, name=None): if id is not None: return Category.objects.get(pk=id) if name is not None: return Category.objects.get(name=name) return None def resolve_ingredient(self, context, id=None, name=None): if id is not None: return Ingredient.objects.get(pk=id) if name is not None: return Ingredient.objects.get(name=name) return None django-graphene-3.2.3/examples/cookbook-plain/cookbook/ingredients/tests.py000066400000000000000000000000321476451357100271420ustar00rootroot00000000000000# Create your tests here. django-graphene-3.2.3/examples/cookbook-plain/cookbook/ingredients/views.py000066400000000000000000000000321476451357100271350ustar00rootroot00000000000000# Create your views here. django-graphene-3.2.3/examples/cookbook-plain/cookbook/recipes/000077500000000000000000000000001476451357100245525ustar00rootroot00000000000000django-graphene-3.2.3/examples/cookbook-plain/cookbook/recipes/__init__.py000066400000000000000000000000001476451357100266510ustar00rootroot00000000000000django-graphene-3.2.3/examples/cookbook-plain/cookbook/recipes/admin.py000066400000000000000000000004271476451357100262170ustar00rootroot00000000000000from django.contrib import admin from cookbook.recipes.models import Recipe, RecipeIngredient class RecipeIngredientInline(admin.TabularInline): model = RecipeIngredient @admin.register(Recipe) class RecipeAdmin(admin.ModelAdmin): inlines = [RecipeIngredientInline] django-graphene-3.2.3/examples/cookbook-plain/cookbook/recipes/apps.py000066400000000000000000000002251476451357100260660ustar00rootroot00000000000000from django.apps import AppConfig class RecipesConfig(AppConfig): name = "cookbook.recipes" label = "recipes" verbose_name = "Recipes" django-graphene-3.2.3/examples/cookbook-plain/cookbook/recipes/migrations/000077500000000000000000000000001476451357100267265ustar00rootroot00000000000000django-graphene-3.2.3/examples/cookbook-plain/cookbook/recipes/migrations/0001_initial.py000066400000000000000000000040571476451357100313770ustar00rootroot00000000000000# Generated by Django 1.9 on 2015-12-04 18:20 import django.db.models.deletion from django.db import migrations, models class Migration(migrations.Migration): initial = True dependencies = [ ("ingredients", "0001_initial"), ] operations = [ migrations.CreateModel( name="Recipe", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("title", models.CharField(max_length=100)), ("instructions", models.TextField()), ], ), migrations.CreateModel( name="RecipeIngredient", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("amount", models.FloatField()), ( "unit", models.CharField( choices=[("kg", "Kilograms"), ("l", "Litres"), ("", "Units")], max_length=20, ), ), ( "ingredient", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="used_by", to="ingredients.Ingredient", ), ), ( "recipes", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="amounts", to="recipes.Recipe", ), ), ], ), ] django-graphene-3.2.3/examples/cookbook-plain/cookbook/recipes/migrations/0002_auto_20161104_0106.py000066400000000000000000000013771476451357100323450ustar00rootroot00000000000000# Generated by Django 1.9 on 2016-11-04 01:06 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("recipes", "0001_initial"), ] operations = [ migrations.RenameField( model_name="recipeingredient", old_name="recipes", new_name="recipe", ), migrations.AlterField( model_name="recipeingredient", name="unit", field=models.CharField( choices=[ (b"unit", b"Units"), (b"kg", b"Kilograms"), (b"l", b"Litres"), (b"st", b"Shots"), ], max_length=20, ), ), ] django-graphene-3.2.3/examples/cookbook-plain/cookbook/recipes/migrations/0003_auto_20181018_1728.py000066400000000000000000000011551476451357100323610ustar00rootroot00000000000000# Generated by Django 2.0 on 2018-10-18 17:28 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("recipes", "0002_auto_20161104_0106"), ] operations = [ migrations.AlterField( model_name="recipeingredient", name="unit", field=models.CharField( choices=[ ("unit", "Units"), ("kg", "Kilograms"), ("l", "Litres"), ("st", "Shots"), ], max_length=20, ), ), ] django-graphene-3.2.3/examples/cookbook-plain/cookbook/recipes/migrations/__init__.py000066400000000000000000000000001476451357100310250ustar00rootroot00000000000000django-graphene-3.2.3/examples/cookbook-plain/cookbook/recipes/models.py000066400000000000000000000013171476451357100264110ustar00rootroot00000000000000from django.db import models from ..ingredients.models import Ingredient class Recipe(models.Model): title = models.CharField(max_length=100) instructions = models.TextField() def __str__(self): return self.title class RecipeIngredient(models.Model): recipe = models.ForeignKey(Recipe, related_name="amounts", on_delete=models.CASCADE) ingredient = models.ForeignKey( Ingredient, related_name="used_by", on_delete=models.CASCADE ) amount = models.FloatField() unit = models.CharField( max_length=20, choices=( ("unit", "Units"), ("kg", "Kilograms"), ("l", "Litres"), ("st", "Shots"), ), ) django-graphene-3.2.3/examples/cookbook-plain/cookbook/recipes/schema.py000066400000000000000000000023741476451357100263720ustar00rootroot00000000000000import graphene from graphene_django.types import DjangoObjectType from .models import Recipe, RecipeIngredient class RecipeType(DjangoObjectType): class Meta: model = Recipe fields = "__all__" class RecipeIngredientType(DjangoObjectType): class Meta: model = RecipeIngredient fields = "__all__" class Query: recipe = graphene.Field(RecipeType, id=graphene.Int(), title=graphene.String()) all_recipes = graphene.List(RecipeType) recipeingredient = graphene.Field(RecipeIngredientType, id=graphene.Int()) all_recipeingredients = graphene.List(RecipeIngredientType) def resolve_recipe(self, context, id=None, title=None): if id is not None: return Recipe.objects.get(pk=id) if title is not None: return Recipe.objects.get(title=title) return None def resolve_recipeingredient(self, context, id=None): if id is not None: return RecipeIngredient.objects.get(pk=id) return None def resolve_all_recipes(self, context): return Recipe.objects.all() def resolve_all_recipeingredients(self, context): related = ["recipe", "ingredient"] return RecipeIngredient.objects.select_related(*related).all() django-graphene-3.2.3/examples/cookbook-plain/cookbook/recipes/tests.py000066400000000000000000000000321476451357100262610ustar00rootroot00000000000000# Create your tests here. django-graphene-3.2.3/examples/cookbook-plain/cookbook/recipes/views.py000066400000000000000000000000321476451357100262540ustar00rootroot00000000000000# Create your views here. django-graphene-3.2.3/examples/cookbook-plain/cookbook/schema.py000066400000000000000000000005251476451357100247340ustar00rootroot00000000000000import graphene from graphene_django.debug import DjangoDebug import cookbook.ingredients.schema import cookbook.recipes.schema class Query( cookbook.ingredients.schema.Query, cookbook.recipes.schema.Query, graphene.ObjectType, ): debug = graphene.Field(DjangoDebug, name="_debug") schema = graphene.Schema(query=Query) django-graphene-3.2.3/examples/cookbook-plain/cookbook/settings.py000066400000000000000000000065671476451357100253500ustar00rootroot00000000000000# flake8: noqa """ Django settings for cookbook project. Generated by 'django-admin startproject' using Django 1.9. For more information on this file, see https://docs.djangoproject.com/en/3.2/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/3.2/ref/settings/ """ import os # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = "_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", "graphene_django", "cookbook.ingredients.apps.IngredientsConfig", "cookbook.recipes.apps.RecipesConfig", ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ] GRAPHENE = { "SCHEMA": "cookbook.schema.schema", "SCHEMA_INDENT": 2, "MIDDLEWARE": ("graphene_django.debug.DjangoDebugMiddleware",), } ROOT_URLCONF = "cookbook.urls" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.template.context_processors.debug", "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", ] }, } ] WSGI_APPLICATION = "cookbook.wsgi.application" # Database # https://docs.djangoproject.com/en/3.2/ref/settings/#databases DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": os.path.join(BASE_DIR, "db.sqlite3"), } } # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = "django.db.models.AutoField" # Password validation # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" }, {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, ] # Internationalization # https://docs.djangoproject.com/en/3.2/topics/i18n/ LANGUAGE_CODE = "en-us" TIME_ZONE = "UTC" USE_I18N = True USE_L10N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.2/howto/static-files/ STATIC_URL = "/static/" django-graphene-3.2.3/examples/cookbook-plain/cookbook/urls.py000066400000000000000000000003371476451357100244620ustar00rootroot00000000000000from django.contrib import admin from django.urls import path from graphene_django.views import GraphQLView urlpatterns = [ path("admin/", admin.site.urls), path("graphql/", GraphQLView.as_view(graphiql=True)), ] django-graphene-3.2.3/examples/cookbook-plain/cookbook/wsgi.py000066400000000000000000000006111476451357100244410ustar00rootroot00000000000000""" WSGI config for cookbook project. It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ """ import os from django.core.wsgi import get_wsgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cookbook.settings") application = get_wsgi_application() django-graphene-3.2.3/examples/cookbook-plain/manage.py000077500000000000000000000003731476451357100231220ustar00rootroot00000000000000#!/usr/bin/env python import os import sys if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cookbook.settings") from django.core.management import execute_from_command_line execute_from_command_line(sys.argv) django-graphene-3.2.3/examples/cookbook-plain/requirements.txt000066400000000000000000000000521476451357100245730ustar00rootroot00000000000000django~=3.2 graphene graphene-django>=3.1 django-graphene-3.2.3/examples/cookbook-plain/setup.cfg000066400000000000000000000000551476451357100231330ustar00rootroot00000000000000[flake8] exclude=migrations,.git,__pycache__ django-graphene-3.2.3/examples/cookbook/000077500000000000000000000000001476451357100202115ustar00rootroot00000000000000django-graphene-3.2.3/examples/cookbook/README.md000066400000000000000000000031511476451357100214700ustar00rootroot00000000000000Cookbook Example (Relay) Django Project =============================== This example project demos integration between Graphene and Django. The project contains two apps, one named `ingredients` and another named `recipes`. Getting started --------------- First you'll need to get the source of the project. Do this by cloning the whole Graphene repository: ```bash # Get the example project code git clone https://github.com/graphql-python/graphene-django.git cd graphene-django/examples/cookbook ``` It is good idea (but not required) to create a virtual environment for this project. We'll do this using [virtualenv](http://docs.python-guide.org/en/latest/dev/virtualenvs/) to keep things simple, but you may also find something like [virtualenvwrapper](https://virtualenvwrapper.readthedocs.org/en/latest/) to be useful: ```bash # Create a virtualenv in which we can install the dependencies virtualenv env source env/bin/activate ``` Now we can install our dependencies: ```bash pip install -r requirements.txt ``` Now setup our database: ```bash # Setup the database ./manage.py migrate # Load some example data ./manage.py loaddata ingredients # Create an admin user (useful for logging into the admin UI # at http://127.0.0.1:8000/admin) ./manage.py createsuperuser ``` Now you should be ready to start the server: ```bash ./manage.py runserver ``` Now head on over to [http://127.0.0.1:8000/graphql](http://127.0.0.1:8000/graphql) and run some queries! (See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial-relay/#testing-our-graphql-schema) for some example queries) django-graphene-3.2.3/examples/cookbook/__init__.py000066400000000000000000000000001476451357100223100ustar00rootroot00000000000000django-graphene-3.2.3/examples/cookbook/cookbook/000077500000000000000000000000001476451357100220175ustar00rootroot00000000000000django-graphene-3.2.3/examples/cookbook/cookbook/__init__.py000066400000000000000000000000001476451357100241160ustar00rootroot00000000000000django-graphene-3.2.3/examples/cookbook/cookbook/ingredients/000077500000000000000000000000001476451357100243325ustar00rootroot00000000000000django-graphene-3.2.3/examples/cookbook/cookbook/ingredients/__init__.py000066400000000000000000000000001476451357100264310ustar00rootroot00000000000000django-graphene-3.2.3/examples/cookbook/cookbook/ingredients/admin.py000066400000000000000000000004351476451357100257760ustar00rootroot00000000000000from django.contrib import admin from cookbook.ingredients.models import Category, Ingredient @admin.register(Ingredient) class IngredientAdmin(admin.ModelAdmin): list_display = ("id", "name", "category") list_editable = ("name", "category") admin.site.register(Category) django-graphene-3.2.3/examples/cookbook/cookbook/ingredients/apps.py000066400000000000000000000002451476451357100256500ustar00rootroot00000000000000from django.apps import AppConfig class IngredientsConfig(AppConfig): name = "cookbook.ingredients" label = "ingredients" verbose_name = "Ingredients" django-graphene-3.2.3/examples/cookbook/cookbook/ingredients/fixtures/000077500000000000000000000000001476451357100262035ustar00rootroot00000000000000django-graphene-3.2.3/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json000066400000000000000000000015671476451357100314220ustar00rootroot00000000000000[ { "fields": { "name": "Dairy" }, "model": "ingredients.category", "pk": 1 }, { "fields": { "name": "Meat" }, "model": "ingredients.category", "pk": 2 }, { "fields": { "category": 1, "name": "Eggs", "notes": "Good old eggs" }, "model": "ingredients.ingredient", "pk": 1 }, { "fields": { "category": 1, "name": "Milk", "notes": "Comes from a cow" }, "model": "ingredients.ingredient", "pk": 2 }, { "fields": { "category": 2, "name": "Beef", "notes": "Much like milk, this comes from a cow" }, "model": "ingredients.ingredient", "pk": 3 }, { "fields": { "category": 2, "name": "Chicken", "notes": "Definitely doesn't come from a cow" }, "model": "ingredients.ingredient", "pk": 4 } ] django-graphene-3.2.3/examples/cookbook/cookbook/ingredients/migrations/000077500000000000000000000000001476451357100265065ustar00rootroot00000000000000django-graphene-3.2.3/examples/cookbook/cookbook/ingredients/migrations/0001_initial.py000066400000000000000000000027401476451357100311540ustar00rootroot00000000000000# Generated by Django 1.9 on 2015-12-04 18:15 import django.db.models.deletion from django.db import migrations, models class Migration(migrations.Migration): initial = True dependencies = [] operations = [ migrations.CreateModel( name="Category", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("name", models.CharField(max_length=100)), ], ), migrations.CreateModel( name="Ingredient", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("name", models.CharField(max_length=100)), ("notes", models.TextField()), ( "category", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="ingredients", to="ingredients.Category", ), ), ], ), ] django-graphene-3.2.3/examples/cookbook/cookbook/ingredients/migrations/0002_auto_20161104_0050.py000066400000000000000000000006031476451357100321120ustar00rootroot00000000000000# Generated by Django 1.9 on 2016-11-04 00:50 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("ingredients", "0001_initial"), ] operations = [ migrations.AlterField( model_name="ingredient", name="notes", field=models.TextField(blank=True, null=True), ), ] django-graphene-3.2.3/examples/cookbook/cookbook/ingredients/migrations/__init__.py000066400000000000000000000000001476451357100306050ustar00rootroot00000000000000django-graphene-3.2.3/examples/cookbook/cookbook/ingredients/models.py000066400000000000000000000006741476451357100261760ustar00rootroot00000000000000from django.db import models class Category(models.Model): name = models.CharField(max_length=100) def __str__(self): return self.name class Ingredient(models.Model): name = models.CharField(max_length=100) notes = models.TextField(null=True, blank=True) category = models.ForeignKey( Category, related_name="ingredients", on_delete=models.CASCADE ) def __str__(self): return self.name django-graphene-3.2.3/examples/cookbook/cookbook/ingredients/schema.py000066400000000000000000000022451476451357100261470ustar00rootroot00000000000000from graphene import Node from graphene_django.filter import DjangoFilterConnectionField from graphene_django.types import DjangoObjectType from cookbook.ingredients.models import Category, Ingredient # Graphene will automatically map the Category model's fields onto the CategoryNode. # This is configured in the CategoryNode's Meta class (as you can see below) class CategoryNode(DjangoObjectType): class Meta: model = Category interfaces = (Node,) fields = "__all__" filter_fields = ["name", "ingredients"] class IngredientNode(DjangoObjectType): class Meta: model = Ingredient # Allow for some more advanced filtering here interfaces = (Node,) fields = "__all__" filter_fields = { "name": ["exact", "icontains", "istartswith"], "notes": ["exact", "icontains"], "category": ["exact"], "category__name": ["exact"], } class Query: category = Node.Field(CategoryNode) all_categories = DjangoFilterConnectionField(CategoryNode) ingredient = Node.Field(IngredientNode) all_ingredients = DjangoFilterConnectionField(IngredientNode) django-graphene-3.2.3/examples/cookbook/cookbook/ingredients/tests.py000066400000000000000000000000321476451357100260410ustar00rootroot00000000000000# Create your tests here. django-graphene-3.2.3/examples/cookbook/cookbook/ingredients/views.py000066400000000000000000000000321476451357100260340ustar00rootroot00000000000000# Create your views here. django-graphene-3.2.3/examples/cookbook/cookbook/recipes/000077500000000000000000000000001476451357100234515ustar00rootroot00000000000000django-graphene-3.2.3/examples/cookbook/cookbook/recipes/__init__.py000066400000000000000000000000001476451357100255500ustar00rootroot00000000000000django-graphene-3.2.3/examples/cookbook/cookbook/recipes/admin.py000066400000000000000000000004271476451357100251160ustar00rootroot00000000000000from django.contrib import admin from cookbook.recipes.models import Recipe, RecipeIngredient class RecipeIngredientInline(admin.TabularInline): model = RecipeIngredient @admin.register(Recipe) class RecipeAdmin(admin.ModelAdmin): inlines = [RecipeIngredientInline] django-graphene-3.2.3/examples/cookbook/cookbook/recipes/apps.py000066400000000000000000000002251476451357100247650ustar00rootroot00000000000000from django.apps import AppConfig class RecipesConfig(AppConfig): name = "cookbook.recipes" label = "recipes" verbose_name = "Recipes" django-graphene-3.2.3/examples/cookbook/cookbook/recipes/migrations/000077500000000000000000000000001476451357100256255ustar00rootroot00000000000000django-graphene-3.2.3/examples/cookbook/cookbook/recipes/migrations/0001_initial.py000066400000000000000000000040571476451357100302760ustar00rootroot00000000000000# Generated by Django 1.9 on 2015-12-04 18:20 import django.db.models.deletion from django.db import migrations, models class Migration(migrations.Migration): initial = True dependencies = [ ("ingredients", "0001_initial"), ] operations = [ migrations.CreateModel( name="Recipe", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("title", models.CharField(max_length=100)), ("instructions", models.TextField()), ], ), migrations.CreateModel( name="RecipeIngredient", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("amount", models.FloatField()), ( "unit", models.CharField( choices=[("kg", "Kilograms"), ("l", "Litres"), ("", "Units")], max_length=20, ), ), ( "ingredient", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="used_by", to="ingredients.Ingredient", ), ), ( "recipes", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="amounts", to="recipes.Recipe", ), ), ], ), ] django-graphene-3.2.3/examples/cookbook/cookbook/recipes/migrations/0002_auto_20161104_0106.py000066400000000000000000000013771476451357100312440ustar00rootroot00000000000000# Generated by Django 1.9 on 2016-11-04 01:06 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("recipes", "0001_initial"), ] operations = [ migrations.RenameField( model_name="recipeingredient", old_name="recipes", new_name="recipe", ), migrations.AlterField( model_name="recipeingredient", name="unit", field=models.CharField( choices=[ (b"unit", b"Units"), (b"kg", b"Kilograms"), (b"l", b"Litres"), (b"st", b"Shots"), ], max_length=20, ), ), ] django-graphene-3.2.3/examples/cookbook/cookbook/recipes/migrations/__init__.py000066400000000000000000000000001476451357100277240ustar00rootroot00000000000000django-graphene-3.2.3/examples/cookbook/cookbook/recipes/models.py000066400000000000000000000013321476451357100253050ustar00rootroot00000000000000from django.db import models from cookbook.ingredients.models import Ingredient class Recipe(models.Model): title = models.CharField(max_length=100) instructions = models.TextField() def __unicode__(self): return self.title class RecipeIngredient(models.Model): recipe = models.ForeignKey(Recipe, related_name="amounts", on_delete=models.CASCADE) ingredient = models.ForeignKey( Ingredient, related_name="used_by", on_delete=models.CASCADE ) amount = models.FloatField() unit = models.CharField( max_length=20, choices=( ("unit", "Units"), ("kg", "Kilograms"), ("l", "Litres"), ("st", "Shots"), ), ) django-graphene-3.2.3/examples/cookbook/cookbook/recipes/schema.py000066400000000000000000000017671476451357100252760ustar00rootroot00000000000000from graphene import Node from graphene_django.filter import DjangoFilterConnectionField from graphene_django.types import DjangoObjectType from cookbook.recipes.models import Recipe, RecipeIngredient class RecipeNode(DjangoObjectType): class Meta: model = Recipe interfaces = (Node,) fields = "__all__" filter_fields = ["title", "amounts"] class RecipeIngredientNode(DjangoObjectType): class Meta: model = RecipeIngredient # Allow for some more advanced filtering here interfaces = (Node,) fields = "__all__" filter_fields = { "ingredient__name": ["exact", "icontains", "istartswith"], "recipe": ["exact"], "recipe__title": ["icontains"], } class Query: recipe = Node.Field(RecipeNode) all_recipes = DjangoFilterConnectionField(RecipeNode) recipeingredient = Node.Field(RecipeIngredientNode) all_recipeingredients = DjangoFilterConnectionField(RecipeIngredientNode) django-graphene-3.2.3/examples/cookbook/cookbook/recipes/tests.py000066400000000000000000000000321476451357100251600ustar00rootroot00000000000000# Create your tests here. django-graphene-3.2.3/examples/cookbook/cookbook/recipes/views.py000066400000000000000000000000321476451357100251530ustar00rootroot00000000000000# Create your views here. django-graphene-3.2.3/examples/cookbook/cookbook/schema.py000066400000000000000000000005251476451357100236330ustar00rootroot00000000000000import graphene from graphene_django.debug import DjangoDebug import cookbook.ingredients.schema import cookbook.recipes.schema class Query( cookbook.ingredients.schema.Query, cookbook.recipes.schema.Query, graphene.ObjectType, ): debug = graphene.Field(DjangoDebug, name="_debug") schema = graphene.Schema(query=Query) django-graphene-3.2.3/examples/cookbook/cookbook/settings.py000066400000000000000000000064221476451357100242350ustar00rootroot00000000000000# flake8: noqa """ Django settings for cookbook project. Generated by 'django-admin startproject' using Django 1.9. For more information on this file, see https://docs.djangoproject.com/en/1.9/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/1.9/ref/settings/ """ import os # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = "_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", "graphene_django", "cookbook.ingredients.apps.IngredientsConfig", "cookbook.recipes.apps.RecipesConfig", "django_filters", ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ] GRAPHENE = { "SCHEMA": "cookbook.schema.schema", "SCHEMA_INDENT": 2, "MIDDLEWARE": ("graphene_django.debug.DjangoDebugMiddleware",), } ROOT_URLCONF = "cookbook.urls" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.template.context_processors.debug", "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", ] }, } ] WSGI_APPLICATION = "cookbook.wsgi.application" # Database # https://docs.djangoproject.com/en/1.9/ref/settings/#databases DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": os.path.join(BASE_DIR, "db.sqlite3"), } } # Password validation # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" }, {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, ] # Internationalization # https://docs.djangoproject.com/en/1.9/topics/i18n/ LANGUAGE_CODE = "en-us" TIME_ZONE = "UTC" USE_I18N = True USE_L10N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.9/howto/static-files/ STATIC_URL = "/static/" django-graphene-3.2.3/examples/cookbook/cookbook/urls.py000066400000000000000000000003451476451357100233600ustar00rootroot00000000000000from django.conf.urls import url from django.contrib import admin from graphene_django.views import GraphQLView urlpatterns = [ url(r"^admin/", admin.site.urls), url(r"^graphql$", GraphQLView.as_view(graphiql=True)), ] django-graphene-3.2.3/examples/cookbook/cookbook/wsgi.py000066400000000000000000000006111476451357100233400ustar00rootroot00000000000000""" WSGI config for cookbook project. It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ """ import os from django.core.wsgi import get_wsgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cookbook.settings") application = get_wsgi_application() django-graphene-3.2.3/examples/cookbook/dummy_data.json000066400000000000000000000123711476451357100232340ustar00rootroot00000000000000[ { "fields": { "date_joined": "2016-11-03T18:24:40Z", "email": "asdf@example.com", "first_name": "", "groups": [], "is_active": true, "is_staff": true, "is_superuser": true, "last_login": "2016-11-04T00:46:58Z", "last_name": "", "password": "pbkdf2_sha256$24000$0SgBlSlnbv5c$ijVQipm2aNDlcrTL8Qi3SVNHphTm4HIsDfUi4kn9tog=", "user_permissions": [], "username": "admin" }, "model": "auth.user", "pk": 1 }, { "fields": { "instructions": "https://xkcd.com/720/", "title": "Cheerios With a Shot of Vermouth" }, "model": "recipes.recipe", "pk": 1 }, { "fields": { "instructions": "https://xkcd.com/720/", "title": "Quail Eggs in Whipped Cream and MSG" }, "model": "recipes.recipe", "pk": 2 }, { "fields": { "instructions": "https://xkcd.com/720/", "title": "Deep Fried Skittles" }, "model": "recipes.recipe", "pk": 3 }, { "fields": { "instructions": "https://xkcd.com/720/", "title": "Newt ala Doritos" }, "model": "recipes.recipe", "pk": 4 }, { "fields": { "instructions": "Chop up and add together", "title": "Fruit Salad" }, "model": "recipes.recipe", "pk": 5 }, { "fields": { "amount": 1.0, "ingredient": 9, "recipes": 5, "unit": "unit" }, "model": "recipes.recipeingredient", "pk": 1 }, { "fields": { "amount": 2.0, "ingredient": 10, "recipes": 5, "unit": "unit" }, "model": "recipes.recipeingredient", "pk": 2 }, { "fields": { "amount": 3.0, "ingredient": 7, "recipes": 5, "unit": "unit" }, "model": "recipes.recipeingredient", "pk": 3 }, { "fields": { "amount": 4.0, "ingredient": 8, "recipes": 5, "unit": "unit" }, "model": "recipes.recipeingredient", "pk": 4 }, { "fields": { "amount": 1.0, "ingredient": 5, "recipes": 4, "unit": "kg" }, "model": "recipes.recipeingredient", "pk": 5 }, { "fields": { "amount": 2.0, "ingredient": 6, "recipes": 4, "unit": "l" }, "model": "recipes.recipeingredient", "pk": 6 }, { "fields": { "amount": 1.0, "ingredient": 4, "recipes": 3, "unit": "unit" }, "model": "recipes.recipeingredient", "pk": 7 }, { "fields": { "amount": 1.0, "ingredient": 2, "recipes": 2, "unit": "kg" }, "model": "recipes.recipeingredient", "pk": 8 }, { "fields": { "amount": 2.0, "ingredient": 11, "recipes": 2, "unit": "l" }, "model": "recipes.recipeingredient", "pk": 9 }, { "fields": { "amount": 3.0, "ingredient": 12, "recipes": 2, "unit": "st" }, "model": "recipes.recipeingredient", "pk": 10 }, { "fields": { "amount": 1.0, "ingredient": 1, "recipes": 1, "unit": "kg" }, "model": "recipes.recipeingredient", "pk": 11 }, { "fields": { "amount": 1.0, "ingredient": 3, "recipes": 1, "unit": "st" }, "model": "recipes.recipeingredient", "pk": 12 }, { "fields": { "name": "fruit" }, "model": "ingredients.category", "pk": 1 }, { "fields": { "name": "xkcd" }, "model": "ingredients.category", "pk": 3 }, { "fields": { "category": 3, "name": "Cheerios", "notes": "this is a note" }, "model": "ingredients.ingredient", "pk": 1 }, { "fields": { "category": 3, "name": "Quail Eggs", "notes": "has more notes" }, "model": "ingredients.ingredient", "pk": 2 }, { "fields": { "category": 3, "name": "Vermouth", "notes": "" }, "model": "ingredients.ingredient", "pk": 3 }, { "fields": { "category": 3, "name": "Skittles", "notes": "" }, "model": "ingredients.ingredient", "pk": 4 }, { "fields": { "category": 3, "name": "Newt", "notes": "Braised and Confused" }, "model": "ingredients.ingredient", "pk": 5 }, { "fields": { "category": 3, "name": "Doritos", "notes": "Crushed" }, "model": "ingredients.ingredient", "pk": 6 }, { "fields": { "category": 1, "name": "Apple", "notes": "" }, "model": "ingredients.ingredient", "pk": 7 }, { "fields": { "category": 1, "name": "Orange", "notes": "" }, "model": "ingredients.ingredient", "pk": 8 }, { "fields": { "category": 1, "name": "Banana", "notes": "" }, "model": "ingredients.ingredient", "pk": 9 }, { "fields": { "category": 1, "name": "Grapes", "notes": "" }, "model": "ingredients.ingredient", "pk": 10 }, { "fields": { "category": 3, "name": "Whipped Cream", "notes": "" }, "model": "ingredients.ingredient", "pk": 11 }, { "fields": { "category": 3, "name": "MSG", "notes": "" }, "model": "ingredients.ingredient", "pk": 12 } ] django-graphene-3.2.3/examples/cookbook/manage.py000077500000000000000000000003731476451357100220210ustar00rootroot00000000000000#!/usr/bin/env python import os import sys if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cookbook.settings") from django.core.management import execute_from_command_line execute_from_command_line(sys.argv) django-graphene-3.2.3/examples/cookbook/requirements.txt000066400000000000000000000001361476451357100234750ustar00rootroot00000000000000graphene>=2.1,<3 graphene-django>=2.1,<3 graphql-core>=2.1,<3 django==3.2.25 django-filter>=2 django-graphene-3.2.3/examples/cookbook/setup.cfg000066400000000000000000000000551476451357100220320ustar00rootroot00000000000000[flake8] exclude=migrations,.git,__pycache__ django-graphene-3.2.3/examples/django_test_settings.py000066400000000000000000000012121476451357100231720ustar00rootroot00000000000000import os import sys ROOT_PATH = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, ROOT_PATH + "/examples/") SECRET_KEY = 1 INSTALLED_APPS = [ "graphene_django", "graphene_django.rest_framework", "graphene_django.tests", "examples.starwars", ] DATABASES = { "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": "django_test.sqlite"} } TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [], "APP_DIRS": True, } ] GRAPHENE = {"SCHEMA": "graphene_django.tests.schema_view.schema"} ROOT_URLCONF = "graphene_django.tests.urls" USE_TZ = True django-graphene-3.2.3/examples/starwars/000077500000000000000000000000001476451357100202515ustar00rootroot00000000000000django-graphene-3.2.3/examples/starwars/__init__.py000066400000000000000000000000001476451357100223500ustar00rootroot00000000000000django-graphene-3.2.3/examples/starwars/data.py000066400000000000000000000031011476451357100215270ustar00rootroot00000000000000from .models import Character, Faction, Ship def initialize(): human = Character(name="Human") human.save() droid = Character(name="Droid") droid.save() rebels = Faction(id="1", name="Alliance to Restore the Republic", hero=human) rebels.save() empire = Faction(id="2", name="Galactic Empire", hero=droid) empire.save() xwing = Ship(id="1", name="X-Wing", faction=rebels) xwing.save() human.ship = xwing human.save() ywing = Ship(id="2", name="Y-Wing", faction=rebels) ywing.save() awing = Ship(id="3", name="A-Wing", faction=rebels) awing.save() # Yeah, technically it's Corellian. But it flew in the service of the rebels, # so for the purposes of this demo it's a rebel ship. falcon = Ship(id="4", name="Millennium Falcon", faction=rebels) falcon.save() homeOne = Ship(id="5", name="Home One", faction=rebels) homeOne.save() tieFighter = Ship(id="6", name="TIE Fighter", faction=empire) tieFighter.save() tieInterceptor = Ship(id="7", name="TIE Interceptor", faction=empire) tieInterceptor.save() executor = Ship(id="8", name="Executor", faction=empire) executor.save() def create_ship(ship_name, faction_id): new_ship = Ship(name=ship_name, faction_id=faction_id) new_ship.save() return new_ship def get_ship(_id): return Ship.objects.get(id=_id) def get_ships(): return Ship.objects.all() def get_faction(_id): return Faction.objects.get(id=_id) def get_rebels(): return get_faction(1) def get_empire(): return get_faction(2) django-graphene-3.2.3/examples/starwars/models.py000066400000000000000000000013101476451357100221010ustar00rootroot00000000000000from django.db import models class Character(models.Model): name = models.CharField(max_length=50) ship = models.ForeignKey( "Ship", on_delete=models.CASCADE, blank=True, null=True, related_name="characters", ) def __str__(self): return self.name class Faction(models.Model): name = models.CharField(max_length=50) hero = models.ForeignKey(Character, on_delete=models.CASCADE) def __str__(self): return self.name class Ship(models.Model): name = models.CharField(max_length=50) faction = models.ForeignKey(Faction, on_delete=models.CASCADE, related_name="ships") def __str__(self): return self.name django-graphene-3.2.3/examples/starwars/schema.py000066400000000000000000000040031476451357100220600ustar00rootroot00000000000000import graphene from graphene import Schema, relay from graphene_django import DjangoConnectionField, DjangoObjectType from .data import create_ship, get_empire, get_faction, get_rebels, get_ship, get_ships from .models import ( Character as CharacterModel, Faction as FactionModel, Ship as ShipModel, ) class Ship(DjangoObjectType): class Meta: model = ShipModel interfaces = (relay.Node,) fields = "__all__" @classmethod def get_node(cls, info, id): node = get_ship(id) return node class Character(DjangoObjectType): class Meta: model = CharacterModel fields = "__all__" class Faction(DjangoObjectType): class Meta: model = FactionModel interfaces = (relay.Node,) fields = "__all__" @classmethod def get_node(cls, info, id): return get_faction(id) class IntroduceShip(relay.ClientIDMutation): class Input: ship_name = graphene.String(required=True) faction_id = graphene.String(required=True) ship = graphene.Field(Ship) faction = graphene.Field(Faction) @classmethod def mutate_and_get_payload( cls, root, info, ship_name, faction_id, client_mutation_id=None ): ship = create_ship(ship_name, faction_id) faction = get_faction(faction_id) return IntroduceShip(ship=ship, faction=faction) class Query(graphene.ObjectType): rebels = graphene.Field(Faction) empire = graphene.Field(Faction) node = relay.Node.Field() ships = DjangoConnectionField(Ship, description="All the ships.") def resolve_ships(self, info): return get_ships() def resolve_rebels(self, info): return get_rebels() def resolve_empire(self, info): return get_empire() class Mutation(graphene.ObjectType): introduce_ship = IntroduceShip.Field() # We register the Character Model because if not would be # inaccessible for the schema schema = Schema(query=Query, mutation=Mutation, types=[Ship, Character]) django-graphene-3.2.3/examples/starwars/tests/000077500000000000000000000000001476451357100214135ustar00rootroot00000000000000django-graphene-3.2.3/examples/starwars/tests/__init__.py000066400000000000000000000000001476451357100235120ustar00rootroot00000000000000django-graphene-3.2.3/examples/starwars/tests/test_connections.py000066400000000000000000000022151476451357100253460ustar00rootroot00000000000000import pytest from ..data import initialize from ..schema import schema pytestmark = pytest.mark.django_db def test_correct_fetch_first_ship_rebels(): initialize() query = """ query RebelsShipsQuery { rebels { name, hero { name } ships(first: 1) { edges { node { name } } } } } """ expected = { "rebels": { "name": "Alliance to Restore the Republic", "hero": {"name": "Human"}, "ships": {"edges": [{"node": {"name": "X-Wing"}}]}, } } result = schema.execute(query) assert not result.errors assert result.data == expected def test_correct_list_characters(): initialize() query = """ query RebelsShipsQuery { node(id: "U2hpcDox") { ... on Ship { name characters { name } } } } """ expected = {"node": {"name": "X-Wing", "characters": [{"name": "Human"}]}} result = schema.execute(query) assert not result.errors assert result.data == expected django-graphene-3.2.3/examples/starwars/tests/test_mutation.py000066400000000000000000000025701476451357100246700ustar00rootroot00000000000000import pytest from ..data import initialize from ..schema import schema pytestmark = pytest.mark.django_db def test_mutations(): initialize() query = """ mutation MyMutation { introduceShip(input:{clientMutationId:"abc", shipName: "Peter", factionId: "1"}) { ship { id name } faction { name ships { edges { node { id name } } } } } } """ expected = { "introduceShip": { "ship": {"id": "U2hpcDo5", "name": "Peter"}, "faction": { "name": "Alliance to Restore the Republic", "ships": { "edges": [ {"node": {"id": "U2hpcDox", "name": "X-Wing"}}, {"node": {"id": "U2hpcDoy", "name": "Y-Wing"}}, {"node": {"id": "U2hpcDoz", "name": "A-Wing"}}, {"node": {"id": "U2hpcDo0", "name": "Millennium Falcon"}}, {"node": {"id": "U2hpcDo1", "name": "Home One"}}, {"node": {"id": "U2hpcDo5", "name": "Peter"}}, ] }, }, } } result = schema.execute(query) assert not result.errors assert result.data == expected django-graphene-3.2.3/examples/starwars/tests/test_objectidentification.py000066400000000000000000000040501476451357100272030ustar00rootroot00000000000000import pytest from ..data import initialize from ..schema import schema pytestmark = pytest.mark.django_db def test_correctly_fetches_id_name_rebels(): initialize() query = """ query RebelsQuery { rebels { id name } } """ expected = { "rebels": {"id": "RmFjdGlvbjox", "name": "Alliance to Restore the Republic"} } result = schema.execute(query) assert not result.errors assert result.data == expected def test_correctly_refetches_rebels(): initialize() query = """ query RebelsRefetchQuery { node(id: "RmFjdGlvbjox") { id ... on Faction { name } } } """ expected = { "node": {"id": "RmFjdGlvbjox", "name": "Alliance to Restore the Republic"} } result = schema.execute(query) assert not result.errors assert result.data == expected def test_correctly_fetches_id_name_empire(): initialize() query = """ query EmpireQuery { empire { id name } } """ expected = {"empire": {"id": "RmFjdGlvbjoy", "name": "Galactic Empire"}} result = schema.execute(query) assert not result.errors assert result.data == expected def test_correctly_refetches_empire(): initialize() query = """ query EmpireRefetchQuery { node(id: "RmFjdGlvbjoy") { id ... on Faction { name } } } """ expected = {"node": {"id": "RmFjdGlvbjoy", "name": "Galactic Empire"}} result = schema.execute(query) assert not result.errors assert result.data == expected def test_correctly_refetches_xwing(): initialize() query = """ query XWingRefetchQuery { node(id: "U2hpcDox") { id ... on Ship { name } } } """ expected = {"node": {"id": "U2hpcDox", "name": "X-Wing"}} result = schema.execute(query) assert not result.errors assert result.data == expected django-graphene-3.2.3/graphene_django/000077500000000000000000000000001476451357100177005ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/__init__.py000066400000000000000000000004461476451357100220150ustar00rootroot00000000000000from .fields import DjangoConnectionField, DjangoListField from .types import DjangoObjectType from .utils import bypass_get_queryset __version__ = "3.2.3" __all__ = [ "__version__", "DjangoObjectType", "DjangoListField", "DjangoConnectionField", "bypass_get_queryset", ] django-graphene-3.2.3/graphene_django/compat.py000066400000000000000000000034571476451357100215460ustar00rootroot00000000000000import sys from collections.abc import Callable from pathlib import PurePath # For backwards compatibility, we import JSONField to have it available for import via # this compat module (https://github.com/graphql-python/graphene-django/issues/1428). # Django's JSONField is available in Django 3.2+ (the minimum version we support) from django.db.models import Choices, JSONField class MissingType: def __init__(self, *args, **kwargs): pass try: # Postgres fields are only available in Django with psycopg2 installed # and we cannot have psycopg2 on PyPy from django.contrib.postgres.fields import ( ArrayField, HStoreField, IntegerRangeField, RangeField, ) except ImportError: IntegerRangeField, HStoreField, RangeField = (MissingType,) * 3 # For unit tests we fake ArrayField using JSONFields if any( PurePath(sys.argv[0]).match(p) for p in [ "**/pytest", "**/py.test", "**/pytest/__main__.py", ] ): class ArrayField(JSONField): def __init__(self, *args, **kwargs): if len(args) > 0: self.base_field = args[0] super().__init__(**kwargs) else: ArrayField = MissingType try: from django.utils.choices import normalize_choices except ImportError: def normalize_choices(choices): if isinstance(choices, type) and issubclass(choices, Choices): choices = choices.choices if isinstance(choices, Callable): choices = choices() # In restframework==3.15.0, choices are not passed # as OrderedDict anymore, so it's safer to check # for a dict if isinstance(choices, dict): choices = choices.items() return choices django-graphene-3.2.3/graphene_django/conftest.py000066400000000000000000000005661476451357100221060ustar00rootroot00000000000000import pytest from graphene_django.settings import graphene_settings as gsettings from .registry import reset_global_registry @pytest.fixture(autouse=True) def reset_registry_fixture(db): yield None reset_global_registry() @pytest.fixture() def graphene_settings(): settings = dict(gsettings.__dict__) yield gsettings gsettings.__dict__ = settings django-graphene-3.2.3/graphene_django/constants.py000066400000000000000000000000661476451357100222700ustar00rootroot00000000000000MUTATION_ERRORS_FLAG = "graphene_mutation_has_errors" django-graphene-3.2.3/graphene_django/converter.py000066400000000000000000000442501476451357100222660ustar00rootroot00000000000000import inspect from functools import partial, singledispatch, wraps from django.db import models from django.utils.encoding import force_str from django.utils.functional import Promise from django.utils.module_loading import import_string from graphql import GraphQLError from graphene import ( ID, UUID, Boolean, Date, DateTime, Decimal, Dynamic, Enum, Field, Float, Int, List, NonNull, String, Time, ) from graphene.types.json import JSONString from graphene.types.resolver import get_default_resolver from graphene.types.scalars import BigInt from graphene.utils.str_converters import to_camel_case try: from graphql import assert_name except ImportError: # Support for older versions of graphql from graphql import assert_valid_name as assert_name from graphql.pyutils import register_description from .compat import ArrayField, HStoreField, RangeField, normalize_choices from .fields import DjangoConnectionField, DjangoListField from .settings import graphene_settings from .utils.str_converters import to_const class BlankValueField(Field): def wrap_resolve(self, parent_resolver): resolver = self.resolver or parent_resolver # create custom resolver def blank_field_wrapper(func): @wraps(func) def wrapped_resolver(*args, **kwargs): return_value = func(*args, **kwargs) if return_value == "": return None return return_value return wrapped_resolver return blank_field_wrapper(resolver) class EnumValueField(BlankValueField): def wrap_resolve(self, parent_resolver): resolver = super().wrap_resolve(parent_resolver) # create custom resolver def enum_field_wrapper(func): @wraps(func) def wrapped_resolver(*args, **kwargs): return_value = func(*args, **kwargs) if isinstance(return_value, models.Choices): return_value = return_value.value return return_value return wrapped_resolver return enum_field_wrapper(resolver) def convert_choice_name(name): name = to_const(force_str(name)) try: assert_name(name) except GraphQLError: name = "A_%s" % name return name def get_choices(choices): converted_names = [] choices = normalize_choices(choices) for value, help_text in choices: if isinstance(help_text, (tuple, list)): yield from get_choices(help_text) else: name = convert_choice_name(value) while name in converted_names: name += "_" + str(len(converted_names)) converted_names.append(name) description = str( help_text ) # TODO: translatable description: https://github.com/graphql-python/graphql-core-next/issues/58 yield name, value, description def convert_choices_to_named_enum_with_descriptions(name, choices): choices = list(get_choices(choices)) named_choices = [(c[0], c[1]) for c in choices] named_choices_descriptions = {c[0]: c[2] for c in choices} class EnumWithDescriptionsType: @property def description(self): return str(named_choices_descriptions[self.name]) return_type = Enum( name, list(named_choices), type=EnumWithDescriptionsType, description="An enumeration.", # Temporary fix until https://github.com/graphql-python/graphene/pull/1502 is merged ) return return_type def generate_enum_name(django_model_meta, field): if graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME: # Try and import custom function custom_func = import_string( graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME ) name = custom_func(field) elif graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V2_NAMING is True: name = to_camel_case(f"{django_model_meta.object_name}_{field.name}") else: name = "{app_label}{object_name}{field_name}Choices".format( app_label=to_camel_case(django_model_meta.app_label.title()), object_name=django_model_meta.object_name, field_name=to_camel_case(field.name.title()), ) return name def convert_choice_field_to_enum(field, name=None): if name is None: name = generate_enum_name(field.model._meta, field) choices = field.choices return convert_choices_to_named_enum_with_descriptions(name, choices) def convert_django_field_with_choices( field, registry=None, convert_choices_to_enum=None ): if registry is not None: converted = registry.get_converted_field(field) if converted: return converted choices = getattr(field, "choices", None) if convert_choices_to_enum is None: convert_choices_to_enum = bool( graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CONVERT ) if choices and convert_choices_to_enum: EnumCls = convert_choice_field_to_enum(field) required = not (field.blank or field.null) converted = EnumCls( description=get_django_field_description(field), required=required ).mount_as(EnumValueField) else: converted = convert_django_field(field, registry) if registry is not None: registry.register_converted_field(field, converted) return converted def get_django_field_description(field): return str(field.help_text) if field.help_text else None @singledispatch def convert_django_field(field, registry=None): raise Exception( f"Don't know how to convert the Django field {field} ({field.__class__})" ) @convert_django_field.register(models.CharField) @convert_django_field.register(models.TextField) @convert_django_field.register(models.EmailField) @convert_django_field.register(models.SlugField) @convert_django_field.register(models.URLField) @convert_django_field.register(models.GenericIPAddressField) @convert_django_field.register(models.FileField) @convert_django_field.register(models.FilePathField) def convert_field_to_string(field, registry=None): return String( description=get_django_field_description(field), required=not field.null ) @convert_django_field.register(models.AutoField) @convert_django_field.register(models.BigAutoField) @convert_django_field.register(models.SmallAutoField) def convert_field_to_id(field, registry=None): return ID(description=get_django_field_description(field), required=not field.null) @convert_django_field.register(models.UUIDField) def convert_field_to_uuid(field, registry=None): return UUID( description=get_django_field_description(field), required=not field.null ) @convert_django_field.register(models.BigIntegerField) def convert_big_int_field(field, registry=None): return BigInt(description=field.help_text, required=not field.null) @convert_django_field.register(models.PositiveIntegerField) @convert_django_field.register(models.PositiveSmallIntegerField) @convert_django_field.register(models.SmallIntegerField) @convert_django_field.register(models.IntegerField) def convert_field_to_int(field, registry=None): return Int(description=get_django_field_description(field), required=not field.null) @convert_django_field.register(models.NullBooleanField) @convert_django_field.register(models.BooleanField) def convert_field_to_boolean(field, registry=None): return Boolean( description=get_django_field_description(field), required=not field.null ) @convert_django_field.register(models.DecimalField) def convert_field_to_decimal(field, registry=None): return Decimal( description=get_django_field_description(field), required=not field.null ) @convert_django_field.register(models.FloatField) @convert_django_field.register(models.DurationField) def convert_field_to_float(field, registry=None): return Float( description=get_django_field_description(field), required=not field.null ) @convert_django_field.register(models.DateTimeField) def convert_datetime_to_string(field, registry=None): return DateTime( description=get_django_field_description(field), required=not field.null ) @convert_django_field.register(models.DateField) def convert_date_to_string(field, registry=None): return Date( description=get_django_field_description(field), required=not field.null ) @convert_django_field.register(models.TimeField) def convert_time_to_string(field, registry=None): return Time( description=get_django_field_description(field), required=not field.null ) @convert_django_field.register(models.OneToOneRel) def convert_onetoone_field_to_djangomodel(field, registry=None): from graphene.utils.str_converters import to_snake_case from .types import DjangoObjectType model = field.related_model def dynamic_type(): _type = registry.get_type_for_model(model) if not _type: return class CustomField(Field): def wrap_resolve(self, parent_resolver): """ Implements a custom resolver which goes through the `get_node` method to ensure that it goes through the `get_queryset` method of the DjangoObjectType. """ resolver = super().wrap_resolve(parent_resolver) # If `get_queryset` was not overridden in the DjangoObjectType # or if we explicitly bypass the `get_queryset` method, # we can just return the default resolver. if ( _type.get_queryset.__func__ is DjangoObjectType.get_queryset.__func__ or getattr(resolver, "_bypass_get_queryset", False) ): return resolver def custom_resolver(root, info, **args): # Note: this function is used to resolve 1:1 relation fields is_resolver_awaitable = inspect.iscoroutinefunction(resolver) if is_resolver_awaitable: fk_obj = resolver(root, info, **args) # In case the resolver is a custom awaitable resolver that overwrites # the default Django resolver return fk_obj field_name = to_snake_case(info.field_name) reversed_field_name = root.__class__._meta.get_field( field_name ).remote_field.name try: return _type.get_queryset( _type._meta.model.objects.filter( **{reversed_field_name: root.pk} ), info, ).get() except _type._meta.model.DoesNotExist: return None return custom_resolver return CustomField( _type, required=not field.null, ) return Dynamic(dynamic_type) @convert_django_field.register(models.ManyToManyField) @convert_django_field.register(models.ManyToManyRel) @convert_django_field.register(models.ManyToOneRel) def convert_field_to_list_or_connection(field, registry=None): model = field.related_model def dynamic_type(): _type = registry.get_type_for_model(model) if not _type: return if isinstance(field, models.ManyToManyField): description = get_django_field_description(field) else: description = get_django_field_description(field.field) # If there is a connection, we should transform the field # into a DjangoConnectionField if _type._meta.connection: # Use a DjangoFilterConnectionField if there are # defined filter_fields or a filterset_class in the # DjangoObjectType Meta if _type._meta.filter_fields or _type._meta.filterset_class: from .filter.fields import DjangoFilterConnectionField return DjangoFilterConnectionField( _type, required=True, description=description ) return DjangoConnectionField(_type, required=True, description=description) return DjangoListField( _type, required=True, # A Set is always returned, never None. description=description, ) return Dynamic(dynamic_type) @convert_django_field.register(models.OneToOneField) @convert_django_field.register(models.ForeignKey) def convert_field_to_djangomodel(field, registry=None): from graphene.utils.str_converters import to_snake_case from .types import DjangoObjectType model = field.related_model def dynamic_type(): _type = registry.get_type_for_model(model) if not _type: return class CustomField(Field): def wrap_resolve(self, parent_resolver): """ Implements a custom resolver which goes through the `get_node` method to ensure that it goes through the `get_queryset` method of the DjangoObjectType. """ resolver = super().wrap_resolve(parent_resolver) # If `get_queryset` was not overridden in the DjangoObjectType # or if we explicitly bypass the `get_queryset` method, # we can just return the default resolver. if ( _type.get_queryset.__func__ is DjangoObjectType.get_queryset.__func__ or getattr(resolver, "_bypass_get_queryset", False) ): return resolver def custom_resolver(root, info, **args): # Note: this function is used to resolve FK or 1:1 fields # it does not differentiate between custom-resolved fields # and default resolved fields. # because this is a django foreign key or one-to-one field, the primary-key for # this node can be accessed from the root node. # ex: article.reporter_id # get the name of the id field from the root's model field_name = to_snake_case(info.field_name) db_field_key = root.__class__._meta.get_field(field_name).attname if hasattr(root, db_field_key): # get the object's primary-key from root object_pk = getattr(root, db_field_key) else: return None is_resolver_awaitable = inspect.iscoroutinefunction(resolver) if is_resolver_awaitable: fk_obj = resolver(root, info, **args) # In case the resolver is a custom awaitable resolver that overwrites # the default Django resolver return fk_obj instance_from_get_node = _type.get_node(info, object_pk) if instance_from_get_node is None: # no instance to return return elif ( isinstance(resolver, partial) and resolver.func is get_default_resolver() ): return instance_from_get_node elif resolver is not get_default_resolver(): # Default resolver is overridden # For optimization, add the instance to the resolver setattr(root, field_name, instance_from_get_node) # Explanation: # previously, _type.get_node` is called which results in at least one hit to the database. # But, if we did not pass the instance to the root, calling the resolver will result in # another call to get the instance which results in at least two database queries in total # to resolve this node only. # That's why the value of the object is set in the root so when the object is accessed # in the resolver (root.field_name) it does not access the database unless queried explicitly. fk_obj = resolver(root, info, **args) return fk_obj else: return instance_from_get_node return custom_resolver return CustomField( _type, description=get_django_field_description(field), required=not field.null, ) return Dynamic(dynamic_type) @convert_django_field.register(ArrayField) def convert_postgres_array_to_list(field, registry=None): inner_type = convert_django_field(field.base_field) if not isinstance(inner_type, (List, NonNull)): inner_type = ( NonNull(type(inner_type)) if inner_type.kwargs["required"] else type(inner_type) ) return List( inner_type, description=get_django_field_description(field), required=not field.null, ) @convert_django_field.register(HStoreField) @convert_django_field.register(models.JSONField) def convert_json_field_to_string(field, registry=None): return JSONString( description=get_django_field_description(field), required=not field.null ) @convert_django_field.register(RangeField) def convert_postgres_range_to_string(field, registry=None): inner_type = convert_django_field(field.base_field) if not isinstance(inner_type, (List, NonNull)): inner_type = ( NonNull(type(inner_type)) if inner_type.kwargs["required"] else type(inner_type) ) return List( inner_type, description=get_django_field_description(field), required=not field.null, ) # Register Django lazy()-wrapped values as GraphQL description/help_text. # This is needed for using lazy translations, see https://github.com/graphql-python/graphql-core-next/issues/58. register_description(Promise) django-graphene-3.2.3/graphene_django/debug/000077500000000000000000000000001476451357100207665ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/debug/__init__.py000066400000000000000000000002011476451357100230700ustar00rootroot00000000000000from .middleware import DjangoDebugMiddleware from .types import DjangoDebug __all__ = ["DjangoDebugMiddleware", "DjangoDebug"] django-graphene-3.2.3/graphene_django/debug/exception/000077500000000000000000000000001476451357100227645ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/debug/exception/__init__.py000066400000000000000000000000001476451357100250630ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/debug/exception/formating.py000066400000000000000000000006411476451357100253250ustar00rootroot00000000000000import traceback from django.utils.encoding import force_str from .types import DjangoDebugException def wrap_exception(exception): return DjangoDebugException( message=force_str(exception), exc_type=force_str(type(exception)), stack="".join( traceback.format_exception( exception, value=exception, tb=exception.__traceback__ ) ), ) django-graphene-3.2.3/graphene_django/debug/exception/types.py000066400000000000000000000006011476451357100244770ustar00rootroot00000000000000from graphene import ObjectType, String class DjangoDebugException(ObjectType): class Meta: description = "Represents a single exception raised." exc_type = String(required=True, description="The class of the exception") message = String(required=True, description="The message of the exception") stack = String(required=True, description="The stack trace") django-graphene-3.2.3/graphene_django/debug/middleware.py000066400000000000000000000044631476451357100234640ustar00rootroot00000000000000from django.db import connections from .exception.formating import wrap_exception from .sql.tracking import unwrap_cursor, wrap_cursor from .types import DjangoDebug class DjangoDebugContext: def __init__(self): self.debug_result = None self.results = [] self.object = DjangoDebug(sql=[], exceptions=[]) self.enable_instrumentation() def get_debug_result(self): if not self.debug_result: self.debug_result = self.results self.results = [] return self.on_resolve_all_results() def on_resolve_error(self, value): if hasattr(self, "object"): self.object.exceptions.append(wrap_exception(value)) return value def on_resolve_all_results(self): if self.results: self.debug_result = None return self.get_debug_result() self.disable_instrumentation() return self.object def add_result(self, result): if self.debug_result: self.results.append(result) def enable_instrumentation(self): # This is thread-safe because database connections are thread-local. for connection in connections.all(): wrap_cursor(connection, self) def disable_instrumentation(self): for connection in connections.all(): unwrap_cursor(connection) class DjangoDebugMiddleware: def resolve(self, next, root, info, **args): context = info.context django_debug = getattr(context, "django_debug", None) if not django_debug: if context is None: raise Exception("DjangoDebug cannot be executed in None contexts") try: context.django_debug = DjangoDebugContext() except Exception: raise Exception( "DjangoDebug need the context to be writable, context received: {}.".format( context.__class__.__name__ ) ) if info.schema.get_type("DjangoDebug") == info.return_type: return context.django_debug.get_debug_result() try: result = next(root, info, **args) except Exception as e: return context.django_debug.on_resolve_error(e) context.django_debug.add_result(result) return result django-graphene-3.2.3/graphene_django/debug/sql/000077500000000000000000000000001476451357100215655ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/debug/sql/__init__.py000066400000000000000000000000001476451357100236640ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/debug/sql/tracking.py000066400000000000000000000115131476451357100237420ustar00rootroot00000000000000# Code obtained from django-debug-toolbar sql panel tracking import json from threading import local from time import time from django.utils.encoding import force_str from .types import DjangoDebugSQL class SQLQueryTriggered(Exception): """Thrown when template panel triggers a query""" class ThreadLocalState(local): def __init__(self): self.enabled = True @property def Wrapper(self): if self.enabled: return NormalCursorWrapper return ExceptionCursorWrapper def recording(self, v): self.enabled = v state = ThreadLocalState() recording = state.recording # export function def wrap_cursor(connection, panel): if not hasattr(connection, "_graphene_cursor"): connection._graphene_cursor = connection.cursor def cursor(): return state.Wrapper(connection._graphene_cursor(), connection, panel) connection.cursor = cursor return cursor def unwrap_cursor(connection): if hasattr(connection, "_graphene_cursor"): previous_cursor = connection._graphene_cursor connection.cursor = previous_cursor del connection._graphene_cursor class ExceptionCursorWrapper: """ Wraps a cursor and raises an exception on any operation. Used in Templates panel. """ def __init__(self, cursor, db, logger): pass def __getattr__(self, attr): raise SQLQueryTriggered() class NormalCursorWrapper: """ Wraps a cursor and logs queries. """ def __init__(self, cursor, db, logger): self.cursor = cursor # Instance of a BaseDatabaseWrapper subclass self.db = db # logger must implement a ``record`` method self.logger = logger def _quote_expr(self, element): if isinstance(element, str): return "'%s'" % force_str(element).replace("'", "''") else: return repr(element) def _quote_params(self, params): if not params: return params if isinstance(params, dict): return {key: self._quote_expr(value) for key, value in params.items()} return list(map(self._quote_expr, params)) def _decode(self, param): try: return force_str(param, strings_only=True) except UnicodeDecodeError: return "(encoded string)" def _record(self, method, sql, params): start_time = time() try: return method(sql, params) finally: stop_time = time() duration = stop_time - start_time _params = "" try: _params = json.dumps(list(map(self._decode, params))) except Exception: pass # object not JSON serializable alias = getattr(self.db, "alias", "default") conn = self.db.connection vendor = getattr(conn, "vendor", "unknown") params = { "vendor": vendor, "alias": alias, "sql": self.db.ops.last_executed_query( self.cursor, sql, self._quote_params(params) ), "duration": duration, "raw_sql": sql, "params": _params, "start_time": start_time, "stop_time": stop_time, "is_slow": duration > 10, "is_select": sql.lower().strip().startswith("select"), } if vendor == "postgresql": # If an erroneous query was ran on the connection, it might # be in a state where checking isolation_level raises an # exception. try: iso_level = conn.isolation_level except conn.InternalError: iso_level = "unknown" params.update( { "trans_id": self.logger.get_transaction_id(alias), "trans_status": conn.get_transaction_status(), "iso_level": iso_level, "encoding": conn.encoding, } ) _sql = DjangoDebugSQL(**params) # We keep `sql` to maintain backwards compatibility self.logger.object.sql.append(_sql) def callproc(self, procname, params=None): return self._record(self.cursor.callproc, procname, params) def execute(self, sql, params=None): return self._record(self.cursor.execute, sql, params) def executemany(self, sql, param_list): return self._record(self.cursor.executemany, sql, param_list) def __getattr__(self, attr): return getattr(self.cursor, attr) def __iter__(self): return iter(self.cursor) def __enter__(self): return self def __exit__(self, type, value, traceback): self.close() django-graphene-3.2.3/graphene_django/debug/sql/types.py000066400000000000000000000031031476451357100233000ustar00rootroot00000000000000from graphene import Boolean, Float, ObjectType, String class DjangoDebugSQL(ObjectType): class Meta: description = "Represents a single database query made to a Django managed DB." vendor = String( required=True, description=( "The type of database being used (e.g. postrgesql, mysql, sqlite)." ), ) alias = String( required=True, description="The Django database alias (e.g. 'default')." ) sql = String(description="The actual SQL sent to this database.") duration = Float( required=True, description="Duration of this database query in seconds." ) raw_sql = String( required=True, description="The raw SQL of this query, without params." ) params = String( required=True, description="JSON encoded database query parameters." ) start_time = Float(required=True, description="Start time of this database query.") stop_time = Float(required=True, description="Stop time of this database query.") is_slow = Boolean( required=True, description="Whether this database query took more than 10 seconds.", ) is_select = Boolean( required=True, description="Whether this database query was a SELECT." ) # Postgres trans_id = String(description="Postgres transaction ID if available.") trans_status = String(description="Postgres transaction status if available.") iso_level = String(description="Postgres isolation level if available.") encoding = String(description="Postgres connection encoding if available.") django-graphene-3.2.3/graphene_django/debug/tests/000077500000000000000000000000001476451357100221305ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/debug/tests/__init__.py000066400000000000000000000000001476451357100242270ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/debug/tests/test_query.py000066400000000000000000000214631476451357100247140ustar00rootroot00000000000000import pytest import graphene from graphene.relay import Node from graphene_django import DjangoConnectionField, DjangoObjectType from ...tests.models import Reporter from ..middleware import DjangoDebugMiddleware from ..types import DjangoDebug class context: pass def test_should_query_field(): r1 = Reporter(last_name="ABA") r1.save() r2 = Reporter(last_name="Griffin") r2.save() class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" class Query(graphene.ObjectType): reporter = graphene.Field(ReporterType) debug = graphene.Field(DjangoDebug, name="_debug") def resolve_reporter(self, info, **args): return Reporter.objects.first() query = """ query ReporterQuery { reporter { lastName } _debug { sql { rawSql } } } """ expected = { "reporter": {"lastName": "ABA"}, "_debug": {"sql": [{"rawSql": str(Reporter.objects.order_by("pk")[:1].query)}]}, } schema = graphene.Schema(query=Query) result = schema.execute( query, context_value=context(), middleware=[DjangoDebugMiddleware()] ) assert not result.errors assert result.data == expected @pytest.mark.parametrize("max_limit", [None, 100]) def test_should_query_nested_field(graphene_settings, max_limit): graphene_settings.RELAY_CONNECTION_MAX_LIMIT = max_limit r1 = Reporter(last_name="ABA") r1.save() r2 = Reporter(last_name="Griffin") r2.save() r2.pets.add(r1) r1.pets.add(r2) class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" class Query(graphene.ObjectType): reporter = graphene.Field(ReporterType) debug = graphene.Field(DjangoDebug, name="_debug") def resolve_reporter(self, info, **args): return Reporter.objects.first() query = """ query ReporterQuery { reporter { lastName pets { edges { node { lastName pets { edges { node { lastName } } } } } } } _debug { sql { rawSql } } } """ expected = { "reporter": { "lastName": "ABA", "pets": { "edges": [ { "node": { "lastName": "Griffin", "pets": {"edges": [{"node": {"lastName": "ABA"}}]}, } } ] }, } } schema = graphene.Schema(query=Query) result = schema.execute( query, context_value=context(), middleware=[DjangoDebugMiddleware()] ) assert not result.errors query = str(Reporter.objects.order_by("pk")[:1].query) assert result.data["_debug"]["sql"][0]["rawSql"] == query assert "COUNT" in result.data["_debug"]["sql"][1]["rawSql"] assert "tests_reporter_pets" in result.data["_debug"]["sql"][2]["rawSql"] assert "COUNT" in result.data["_debug"]["sql"][3]["rawSql"] assert "tests_reporter_pets" in result.data["_debug"]["sql"][4]["rawSql"] assert len(result.data["_debug"]["sql"]) == 5 assert result.data["reporter"] == expected["reporter"] def test_should_query_list(): r1 = Reporter(last_name="ABA") r1.save() r2 = Reporter(last_name="Griffin") r2.save() class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" class Query(graphene.ObjectType): all_reporters = graphene.List(ReporterType) debug = graphene.Field(DjangoDebug, name="_debug") def resolve_all_reporters(self, info, **args): return Reporter.objects.all() query = """ query ReporterQuery { allReporters { lastName } _debug { sql { rawSql } } } """ expected = { "allReporters": [{"lastName": "ABA"}, {"lastName": "Griffin"}], "_debug": {"sql": [{"rawSql": str(Reporter.objects.all().query)}]}, } schema = graphene.Schema(query=Query) result = schema.execute( query, context_value=context(), middleware=[DjangoDebugMiddleware()] ) assert not result.errors assert result.data == expected @pytest.mark.parametrize("max_limit", [None, 100]) def test_should_query_connection(graphene_settings, max_limit): graphene_settings.RELAY_CONNECTION_MAX_LIMIT = max_limit r1 = Reporter(last_name="ABA") r1.save() r2 = Reporter(last_name="Griffin") r2.save() class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) debug = graphene.Field(DjangoDebug, name="_debug") def resolve_all_reporters(self, info, **args): return Reporter.objects.all() query = """ query ReporterQuery { allReporters(first:1) { edges { node { lastName } } } _debug { sql { rawSql } } } """ expected = {"allReporters": {"edges": [{"node": {"lastName": "ABA"}}]}} schema = graphene.Schema(query=Query) result = schema.execute( query, context_value=context(), middleware=[DjangoDebugMiddleware()] ) assert not result.errors assert result.data["allReporters"] == expected["allReporters"] assert len(result.data["_debug"]["sql"]) == 2 assert "COUNT" in result.data["_debug"]["sql"][0]["rawSql"] query = str(Reporter.objects.all()[:1].query) assert result.data["_debug"]["sql"][1]["rawSql"] == query @pytest.mark.parametrize("max_limit", [None, 100]) def test_should_query_connectionfilter(graphene_settings, max_limit): graphene_settings.RELAY_CONNECTION_MAX_LIMIT = max_limit from ...filter import DjangoFilterConnectionField r1 = Reporter(last_name="ABA") r1.save() r2 = Reporter(last_name="Griffin") r2.save() class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" class Query(graphene.ObjectType): all_reporters = DjangoFilterConnectionField(ReporterType, fields=["last_name"]) s = graphene.String(resolver=lambda *_: "S") debug = graphene.Field(DjangoDebug, name="_debug") def resolve_all_reporters(self, info, **args): return Reporter.objects.all() query = """ query ReporterQuery { allReporters(first:1) { edges { node { lastName } } } _debug { sql { rawSql } } } """ expected = {"allReporters": {"edges": [{"node": {"lastName": "ABA"}}]}} schema = graphene.Schema(query=Query) result = schema.execute( query, context_value=context(), middleware=[DjangoDebugMiddleware()] ) assert not result.errors assert result.data["allReporters"] == expected["allReporters"] assert len(result.data["_debug"]["sql"]) == 2 assert "COUNT" in result.data["_debug"]["sql"][0]["rawSql"] query = str(Reporter.objects.all()[:1].query) assert result.data["_debug"]["sql"][1]["rawSql"] == query def test_should_query_stack_trace(): class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" class Query(graphene.ObjectType): reporter = graphene.Field(ReporterType) debug = graphene.Field(DjangoDebug, name="_debug") def resolve_reporter(self, info, **args): raise Exception("caught stack trace") query = """ query ReporterQuery { reporter { lastName } _debug { exceptions { message stack } } } """ schema = graphene.Schema(query=Query) result = schema.execute( query, context_value=context(), middleware=[DjangoDebugMiddleware()] ) assert result.errors assert len(result.data["_debug"]["exceptions"]) debug_exception = result.data["_debug"]["exceptions"][0] assert debug_exception["stack"].count("\n") > 1 assert "test_query.py" in debug_exception["stack"] assert debug_exception["message"] == "caught stack trace" django-graphene-3.2.3/graphene_django/debug/types.py000066400000000000000000000006731476451357100225120ustar00rootroot00000000000000from graphene import List, ObjectType from .exception.types import DjangoDebugException from .sql.types import DjangoDebugSQL class DjangoDebug(ObjectType): class Meta: description = "Debugging information for the current query." sql = List(DjangoDebugSQL, description="Executed SQL queries for this API query.") exceptions = List( DjangoDebugException, description="Raise exceptions for this API query." ) django-graphene-3.2.3/graphene_django/fields.py000066400000000000000000000206221476451357100215220ustar00rootroot00000000000000from functools import partial from django.db.models.query import QuerySet from graphql_relay import ( connection_from_array_slice, cursor_to_offset, get_offset_with_default, offset_to_cursor, ) from promise import Promise from graphene import Int, NonNull from graphene.relay import ConnectionField from graphene.relay.connection import connection_adapter, page_info_adapter from graphene.types import Field, List from .settings import graphene_settings from .utils import maybe_queryset class DjangoListField(Field): def __init__(self, _type, *args, **kwargs): if isinstance(_type, NonNull): _type = _type.of_type # Django would never return a Set of None vvvvvvv super().__init__(List(NonNull(_type)), *args, **kwargs) @property def type(self): from .types import DjangoObjectType assert issubclass( self._underlying_type, DjangoObjectType ), "DjangoListField only accepts DjangoObjectType types as underlying type" return super().type @property def _underlying_type(self): _type = self._type while hasattr(_type, "of_type"): _type = _type.of_type return _type @property def model(self): return self._underlying_type._meta.model def get_manager(self): return self.model._default_manager @staticmethod def list_resolver( django_object_type, resolver, default_manager, root, info, **args ): queryset = maybe_queryset(resolver(root, info, **args)) if queryset is None: queryset = maybe_queryset(default_manager) if isinstance(queryset, QuerySet): # Pass queryset to the DjangoObjectType get_queryset method queryset = maybe_queryset(django_object_type.get_queryset(queryset, info)) return queryset def wrap_resolve(self, parent_resolver): resolver = super().wrap_resolve(parent_resolver) _type = self.type if isinstance(_type, NonNull): _type = _type.of_type django_object_type = _type.of_type.of_type return partial( self.list_resolver, django_object_type, resolver, self.get_manager(), ) class DjangoConnectionField(ConnectionField): def __init__(self, *args, **kwargs): self.on = kwargs.pop("on", False) self.max_limit = kwargs.pop( "max_limit", graphene_settings.RELAY_CONNECTION_MAX_LIMIT ) self.enforce_first_or_last = kwargs.pop( "enforce_first_or_last", graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST, ) kwargs.setdefault("offset", Int()) super().__init__(*args, **kwargs) @property def type(self): from .types import DjangoObjectType _type = super(ConnectionField, self).type non_null = False if isinstance(_type, NonNull): _type = _type.of_type non_null = True assert issubclass( _type, DjangoObjectType ), "DjangoConnectionField only accepts DjangoObjectType types" assert _type._meta.connection, "The type {} doesn't have a connection".format( _type.__name__ ) connection_type = _type._meta.connection if non_null: return NonNull(connection_type) return connection_type @property def connection_type(self): type = self.type if isinstance(type, NonNull): return type.of_type return type @property def node_type(self): return self.connection_type._meta.node @property def model(self): return self.node_type._meta.model def get_manager(self): if self.on: return getattr(self.model, self.on) else: return self.model._default_manager @classmethod def resolve_queryset(cls, connection, queryset, info, args): # queryset is the resolved iterable from ObjectType return connection._meta.node.get_queryset(queryset, info) @classmethod def resolve_connection(cls, connection, args, iterable, max_limit=None): # Remove the offset parameter and convert it to an after cursor. offset = args.pop("offset", None) after = args.get("after") if offset: if after: offset += cursor_to_offset(after) + 1 # input offset starts at 1 while the graphene offset starts at 0 args["after"] = offset_to_cursor(offset - 1) iterable = maybe_queryset(iterable) if isinstance(iterable, QuerySet): array_length = iterable.count() else: array_length = len(iterable) # If after is higher than array_length, connection_from_array_slice # would try to do a negative slicing which makes django throw an # AssertionError slice_start = min( get_offset_with_default(args.get("after"), -1) + 1, array_length, ) array_slice_length = array_length - slice_start # Impose the maximum limit via the `first` field if neither first or last are already provided # (note that if any of them is provided they must be under max_limit otherwise an error is raised). if ( max_limit is not None and args.get("first", None) is None and args.get("last", None) is None ): args["first"] = max_limit connection = connection_from_array_slice( iterable[slice_start:], args, slice_start=slice_start, array_length=array_length, array_slice_length=array_slice_length, connection_type=partial(connection_adapter, connection), edge_type=connection.Edge, page_info_type=page_info_adapter, ) connection.iterable = iterable connection.length = array_length return connection @classmethod def connection_resolver( cls, resolver, connection, default_manager, queryset_resolver, max_limit, enforce_first_or_last, root, info, **args, ): first = args.get("first") last = args.get("last") offset = args.get("offset") before = args.get("before") if enforce_first_or_last: assert first or last, ( "You must provide a `first` or `last` value to properly paginate the `{}` connection." ).format(info.field_name) if max_limit: if first: assert first <= max_limit, ( "Requesting {} records on the `{}` connection exceeds the `first` limit of {} records." ).format(first, info.field_name, max_limit) args["first"] = min(first, max_limit) if last: assert last <= max_limit, ( "Requesting {} records on the `{}` connection exceeds the `last` limit of {} records." ).format(last, info.field_name, max_limit) args["last"] = min(last, max_limit) if offset is not None: assert before is None, ( "You can't provide a `before` value at the same time as an `offset` value to properly paginate the `{}` connection." ).format(info.field_name) # eventually leads to DjangoObjectType's get_queryset (accepts queryset) # or a resolve_foo (does not accept queryset) iterable = resolver(root, info, **args) if iterable is None: iterable = default_manager # thus the iterable gets refiltered by resolve_queryset # but iterable might be promise iterable = queryset_resolver(connection, iterable, info, args) on_resolve = partial( cls.resolve_connection, connection, args, max_limit=max_limit ) if Promise.is_thenable(iterable): return Promise.resolve(iterable).then(on_resolve) return on_resolve(iterable) def wrap_resolve(self, parent_resolver): return partial( self.connection_resolver, self.resolver or parent_resolver, self.connection_type, self.get_manager(), self.get_queryset_resolver(), self.max_limit, self.enforce_first_or_last, ) def get_queryset_resolver(self): return self.resolve_queryset django-graphene-3.2.3/graphene_django/filter/000077500000000000000000000000001476451357100211655ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/filter/__init__.py000066400000000000000000000013441476451357100233000ustar00rootroot00000000000000import warnings from ..utils import DJANGO_FILTER_INSTALLED if not DJANGO_FILTER_INSTALLED: warnings.warn( "Use of django filtering requires the django-filter package " "be installed. You can do so using `pip install django-filter`", ImportWarning, ) else: from .fields import DjangoFilterConnectionField from .filters import ( ArrayFilter, GlobalIDFilter, GlobalIDMultipleChoiceFilter, ListFilter, RangeFilter, TypedFilter, ) __all__ = [ "DjangoFilterConnectionField", "GlobalIDFilter", "GlobalIDMultipleChoiceFilter", "ArrayFilter", "ListFilter", "RangeFilter", "TypedFilter", ] django-graphene-3.2.3/graphene_django/filter/fields.py000066400000000000000000000065211476451357100230110ustar00rootroot00000000000000from collections import OrderedDict from functools import partial from django.core.exceptions import ValidationError from graphene.types.argument import to_arguments from graphene.types.enum import EnumType from graphene.utils.str_converters import to_snake_case from ..fields import DjangoConnectionField from .utils import get_filtering_args_from_filterset, get_filterset_class def convert_enum(data): """ Check if the data is a enum option (or potentially nested list of enum option) and convert it to its value. This method is used to pre-process the data for the filters as they can take an graphene.Enum as argument, but filters (from django_filters) expect a simple value. """ if isinstance(data, list): return [convert_enum(item) for item in data] if isinstance(type(data), EnumType): return data.value else: return data class DjangoFilterConnectionField(DjangoConnectionField): def __init__( self, type_, fields=None, order_by=None, extra_filter_meta=None, filterset_class=None, *args, **kwargs, ): self._fields = fields self._provided_filterset_class = filterset_class self._filterset_class = None self._filtering_args = None self._extra_filter_meta = extra_filter_meta self._base_args = None super().__init__(type_, *args, **kwargs) @property def args(self): return to_arguments(self._base_args or OrderedDict(), self.filtering_args) @args.setter def args(self, args): self._base_args = args @property def filterset_class(self): if not self._filterset_class: fields = self._fields or self.node_type._meta.filter_fields meta = {"model": self.model, "fields": fields} if self._extra_filter_meta: meta.update(self._extra_filter_meta) filterset_class = ( self._provided_filterset_class or self.node_type._meta.filterset_class ) self._filterset_class = get_filterset_class(filterset_class, **meta) return self._filterset_class @property def filtering_args(self): if not self._filtering_args: self._filtering_args = get_filtering_args_from_filterset( self.filterset_class, self.node_type ) return self._filtering_args @classmethod def resolve_queryset( cls, connection, iterable, info, args, filtering_args, filterset_class ): def filter_kwargs(): kwargs = {} for k, v in args.items(): if k in filtering_args: if k == "order_by" and v is not None: v = to_snake_case(v) kwargs[k] = convert_enum(v) return kwargs qs = super().resolve_queryset(connection, iterable, info, args) filterset = filterset_class( data=filter_kwargs(), queryset=qs, request=info.context ) if filterset.is_valid(): return filterset.qs raise ValidationError(filterset.form.errors.as_json()) def get_queryset_resolver(self): return partial( self.resolve_queryset, filterset_class=self.filterset_class, filtering_args=self.filtering_args, ) django-graphene-3.2.3/graphene_django/filter/filters/000077500000000000000000000000001476451357100226355ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/filter/filters/__init__.py000066400000000000000000000013641476451357100247520ustar00rootroot00000000000000import warnings from ...utils import DJANGO_FILTER_INSTALLED if not DJANGO_FILTER_INSTALLED: warnings.warn( "Use of django filtering requires the django-filter package " "be installed. You can do so using `pip install django-filter`", ImportWarning, ) else: from .array_filter import ArrayFilter from .global_id_filter import GlobalIDFilter, GlobalIDMultipleChoiceFilter from .list_filter import ListFilter from .range_filter import RangeFilter from .typed_filter import TypedFilter __all__ = [ "DjangoFilterConnectionField", "GlobalIDFilter", "GlobalIDMultipleChoiceFilter", "ArrayFilter", "ListFilter", "RangeFilter", "TypedFilter", ] django-graphene-3.2.3/graphene_django/filter/filters/array_filter.py000066400000000000000000000037201476451357100256740ustar00rootroot00000000000000from django_filters.constants import EMPTY_VALUES from django_filters.filters import FilterMethod from .typed_filter import TypedFilter class ArrayFilterMethod(FilterMethod): def __call__(self, qs, value): if value is None: return qs return self.method(qs, self.f.field_name, value) class ArrayFilter(TypedFilter): """ Filter made for PostgreSQL ArrayField. """ @TypedFilter.method.setter def method(self, value): """ Override method setter so that in case a custom `method` is provided (see documentation https://django-filter.readthedocs.io/en/stable/ref/filters.html#method), it doesn't fall back to checking if the value is in `EMPTY_VALUES` (from the `__call__` method of the `FilterMethod` class) and instead use our ArrayFilterMethod that consider empty lists as values. Indeed when providing a `method` the `filter` method below is overridden and replaced by `FilterMethod(self)` which means that the validation of the empty value is made by the `FilterMethod.__call__` method instead. """ TypedFilter.method.fset(self, value) if value is not None: self.filter = ArrayFilterMethod(self) def filter(self, qs, value): """ Override the default filter class to check first whether the list is empty or not. This needs to be done as in this case we expect to get the filter applied with an empty list since it's a valid value but django_filter consider an empty list to be an empty input value (see `EMPTY_VALUES`) meaning that the filter does not need to be applied (hence returning the original queryset). """ if value in EMPTY_VALUES and value != []: return qs if self.distinct: qs = qs.distinct() lookup = f"{self.field_name}__{self.lookup_expr}" qs = self.get_method(qs)(**{lookup: value}) return qs django-graphene-3.2.3/graphene_django/filter/filters/global_id_filter.py000066400000000000000000000013741476451357100264750ustar00rootroot00000000000000from django_filters import Filter, MultipleChoiceFilter from graphql_relay.node.node import from_global_id from ...forms import GlobalIDFormField, GlobalIDMultipleChoiceField class GlobalIDFilter(Filter): """ Filter for Relay global ID. """ field_class = GlobalIDFormField def filter(self, qs, value): """Convert the filter value to a primary key before filtering""" _id = None if value is not None: _, _id = from_global_id(value) return super().filter(qs, _id) class GlobalIDMultipleChoiceFilter(MultipleChoiceFilter): field_class = GlobalIDMultipleChoiceField def filter(self, qs, value): gids = [from_global_id(v)[1] for v in value] return super().filter(qs, gids) django-graphene-3.2.3/graphene_django/filter/filters/list_filter.py000066400000000000000000000036201476451357100255300ustar00rootroot00000000000000from django_filters.filters import FilterMethod from .typed_filter import TypedFilter class ListFilterMethod(FilterMethod): def __call__(self, qs, value): if value is None: return qs return self.method(qs, self.f.field_name, value) class ListFilter(TypedFilter): """ Filter that takes a list of value as input. It is for example used for `__in` filters. """ @TypedFilter.method.setter def method(self, value): """ Override method setter so that in case a custom `method` is provided (see documentation https://django-filter.readthedocs.io/en/stable/ref/filters.html#method), it doesn't fall back to checking if the value is in `EMPTY_VALUES` (from the `__call__` method of the `FilterMethod` class) and instead use our ListFilterMethod that consider empty lists as values. Indeed when providing a `method` the `filter` method below is overridden and replaced by `FilterMethod(self)` which means that the validation of the empty value is made by the `FilterMethod.__call__` method instead. """ TypedFilter.method.fset(self, value) if value is not None: self.filter = ListFilterMethod(self) def filter(self, qs, value): """ Override the default filter class to check first whether the list is empty or not. This needs to be done as in this case we expect to get an empty output (if not an exclude filter) but django_filter consider an empty list to be an empty input value (see `EMPTY_VALUES`) meaning that the filter does not need to be applied (hence returning the original queryset). """ if value is not None and len(value) == 0: if self.exclude: return qs else: return qs.none() else: return super().filter(qs, value) django-graphene-3.2.3/graphene_django/filter/filters/range_filter.py000066400000000000000000000011501476451357100256450ustar00rootroot00000000000000from django.core.exceptions import ValidationError from django.forms import Field from .typed_filter import TypedFilter def validate_range(value): """ Validator for range filter input: the list of value must be of length 2. Note that validators are only run if the value is not empty. """ if len(value) != 2: raise ValidationError( "Invalid range specified: it needs to contain 2 values.", code="invalid" ) class RangeField(Field): default_validators = [validate_range] empty_values = [None] class RangeFilter(TypedFilter): field_class = RangeField django-graphene-3.2.3/graphene_django/filter/filters/typed_filter.py000066400000000000000000000015641476451357100257070ustar00rootroot00000000000000from django_filters import Filter from graphene.types.utils import get_type class TypedFilter(Filter): """ Filter class for which the input GraphQL type can explicitly be provided. If it is not provided, when building the schema, it will try to guess it from the field. """ def __init__(self, input_type=None, *args, **kwargs): self._input_type = input_type super().__init__(*args, **kwargs) @property def input_type(self): input_type = get_type(self._input_type) if input_type is not None: if not callable(getattr(input_type, "get_type", None)): raise ValueError( "Wrong `input_type` for {}: it only accepts graphene types, got {}".format( self.__class__.__name__, input_type ) ) return input_type django-graphene-3.2.3/graphene_django/filter/filterset.py000066400000000000000000000031441476451357100235420ustar00rootroot00000000000000import itertools from django.db import models from django_filters.filterset import ( FILTER_FOR_DBFIELD_DEFAULTS, BaseFilterSet, FilterSet, ) from .filters import GlobalIDFilter, GlobalIDMultipleChoiceFilter GRAPHENE_FILTER_SET_OVERRIDES = { models.AutoField: {"filter_class": GlobalIDFilter}, models.OneToOneField: {"filter_class": GlobalIDFilter}, models.ForeignKey: {"filter_class": GlobalIDFilter}, models.ManyToManyField: {"filter_class": GlobalIDMultipleChoiceFilter}, models.ManyToOneRel: {"filter_class": GlobalIDMultipleChoiceFilter}, models.ManyToManyRel: {"filter_class": GlobalIDMultipleChoiceFilter}, } class GrapheneFilterSetMixin(BaseFilterSet): """A django_filters.filterset.BaseFilterSet with default filter overrides to handle global IDs""" FILTER_DEFAULTS = dict( itertools.chain( FILTER_FOR_DBFIELD_DEFAULTS.items(), GRAPHENE_FILTER_SET_OVERRIDES.items() ) ) def setup_filterset(filterset_class): """Wrap a provided filterset in Graphene-specific functionality""" return type( f"Graphene{filterset_class.__name__}", (filterset_class, GrapheneFilterSetMixin), {}, ) def custom_filterset_factory(model, filterset_base_class=FilterSet, **meta): """Create a filterset for the given model using the provided meta data""" meta.update({"model": model}) meta_class = type("Meta", (object,), meta) filterset = type( str("%sFilterSet" % model._meta.object_name), (filterset_base_class, GrapheneFilterSetMixin), {"Meta": meta_class}, ) return filterset django-graphene-3.2.3/graphene_django/filter/tests/000077500000000000000000000000001476451357100223275ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/filter/tests/__init__.py000066400000000000000000000000001476451357100244260ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/filter/tests/conftest.py000066400000000000000000000146651476451357100245420ustar00rootroot00000000000000from functools import reduce import pytest from django.db import models from django.db.models.query import QuerySet from django_filters import FilterSet import graphene from graphene.relay import Node from graphene_django import DjangoObjectType from graphene_django.filter import ArrayFilter from graphene_django.utils import DJANGO_FILTER_INSTALLED from ...compat import ArrayField pytestmark = [] if DJANGO_FILTER_INSTALLED: from graphene_django.filter import DjangoFilterConnectionField else: pytestmark.append( pytest.mark.skipif( True, reason="django_filters not installed or not compatible" ) ) class Event(models.Model): name = models.CharField(max_length=50) tags = ArrayField(models.CharField(max_length=50)) tag_ids = ArrayField(models.IntegerField()) random_field = ArrayField(models.BooleanField()) def __repr__(self): return f"Event [{self.name}]" @pytest.fixture def EventFilterSet(): class EventFilterSet(FilterSet): class Meta: model = Event fields = { "name": ["exact", "contains"], } # Those are actually usable with our Query fixture below tags__contains = ArrayFilter(field_name="tags", lookup_expr="contains") tags__overlap = ArrayFilter(field_name="tags", lookup_expr="overlap") tags = ArrayFilter(field_name="tags", lookup_expr="exact") tags__len = ArrayFilter( field_name="tags", lookup_expr="len", input_type=graphene.Int ) tags__len__in = ArrayFilter( field_name="tags", method="tags__len__in_filter", input_type=graphene.List(graphene.Int), ) # Those are actually not usable and only to check type declarations tags_ids__contains = ArrayFilter(field_name="tag_ids", lookup_expr="contains") tags_ids__overlap = ArrayFilter(field_name="tag_ids", lookup_expr="overlap") tags_ids = ArrayFilter(field_name="tag_ids", lookup_expr="exact") random_field__contains = ArrayFilter( field_name="random_field", lookup_expr="contains" ) random_field__overlap = ArrayFilter( field_name="random_field", lookup_expr="overlap" ) random_field = ArrayFilter(field_name="random_field", lookup_expr="exact") def tags__len__in_filter(self, queryset, _name, value): if not value: return queryset.none() return reduce( lambda q1, q2: q1.union(q2), [queryset.filter(tags__len=v) for v in value], ).distinct() return EventFilterSet @pytest.fixture def EventType(EventFilterSet): class EventType(DjangoObjectType): class Meta: model = Event interfaces = (Node,) fields = "__all__" filterset_class = EventFilterSet return EventType @pytest.fixture def Query(EventType): """ Note that we have to use a custom resolver to replicate the arrayfield filter behavior as we are running unit tests in sqlite which does not have ArrayFields. """ events = [ Event(name="Live Show", tags=["concert", "music", "rock"]), Event(name="Musical", tags=["movie", "music"]), Event(name="Ballet", tags=["concert", "dance"]), Event(name="Speech", tags=[]), ] class Query(graphene.ObjectType): events = DjangoFilterConnectionField(EventType) def resolve_events(self, info, **kwargs): class FakeQuerySet(QuerySet): def __init__(self, model=None): self.model = Event self.__store = list(events) def all(self): return self def filter(self, **kwargs): queryset = FakeQuerySet() queryset.__store = list(self.__store) if "tags__contains" in kwargs: queryset.__store = list( filter( lambda e: set(kwargs["tags__contains"]).issubset( set(e.tags) ), queryset.__store, ) ) if "tags__overlap" in kwargs: queryset.__store = list( filter( lambda e: not set(kwargs["tags__overlap"]).isdisjoint( set(e.tags) ), queryset.__store, ) ) if "tags__exact" in kwargs: queryset.__store = list( filter( lambda e: set(kwargs["tags__exact"]) == set(e.tags), queryset.__store, ) ) if "tags__len" in kwargs: queryset.__store = list( filter( lambda e: len(e.tags) == kwargs["tags__len"], queryset.__store, ) ) return queryset def union(self, *args): queryset = FakeQuerySet() queryset.__store = self.__store for arg in args: queryset.__store += arg.__store return queryset def none(self): queryset = FakeQuerySet() queryset.__store = [] return queryset def count(self): return len(self.__store) def distinct(self): queryset = FakeQuerySet() queryset.__store = [] for event in self.__store: if event not in queryset.__store: queryset.__store.append(event) queryset.__store = sorted(queryset.__store, key=lambda e: e.name) return queryset def __getitem__(self, index): return self.__store[index] return FakeQuerySet() return Query @pytest.fixture def schema(Query): return graphene.Schema(query=Query) django-graphene-3.2.3/graphene_django/filter/tests/filters.py000066400000000000000000000013571476451357100243570ustar00rootroot00000000000000import django_filters from django_filters import OrderingFilter from graphene_django.tests.models import Article, Pet, Reporter class ArticleFilter(django_filters.FilterSet): class Meta: model = Article fields = { "headline": ["exact", "icontains"], "pub_date": ["gt", "lt", "exact"], "reporter": ["exact", "in"], } order_by = OrderingFilter(fields=("pub_date",)) class ReporterFilter(django_filters.FilterSet): class Meta: model = Reporter fields = ["first_name", "last_name", "email", "pets"] order_by = OrderingFilter(fields=("first_name",)) class PetFilter(django_filters.FilterSet): class Meta: model = Pet fields = ["name"] django-graphene-3.2.3/graphene_django/filter/tests/test_array_field_contains_filter.py000066400000000000000000000035311476451357100314660ustar00rootroot00000000000000import pytest from ...compat import ArrayField, MissingType @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") def test_array_field_contains_multiple(schema): """ Test contains filter on a array field of string. """ query = """ query { events (tags_Contains: ["concert", "music"]) { edges { node { name } } } } """ result = schema.execute(query) assert not result.errors assert result.data["events"]["edges"] == [ {"node": {"name": "Live Show"}}, ] @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") def test_array_field_contains_one(schema): """ Test contains filter on a array field of string. """ query = """ query { events (tags_Contains: ["music"]) { edges { node { name } } } } """ result = schema.execute(query) assert not result.errors assert result.data["events"]["edges"] == [ {"node": {"name": "Live Show"}}, {"node": {"name": "Musical"}}, ] @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") def test_array_field_contains_empty_list(schema): """ Test contains filter on a array field of string. """ query = """ query { events (tags_Contains: []) { edges { node { name } } } } """ result = schema.execute(query) assert not result.errors assert result.data["events"]["edges"] == [ {"node": {"name": "Live Show"}}, {"node": {"name": "Musical"}}, {"node": {"name": "Ballet"}}, {"node": {"name": "Speech"}}, ] django-graphene-3.2.3/graphene_django/filter/tests/test_array_field_custom_filter.py000066400000000000000000000100131476451357100311530ustar00rootroot00000000000000import pytest from ...compat import ArrayField, MissingType @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") def test_array_field_len_filter(schema): query = """ query { events (tags_Len: 2) { edges { node { name } } } } """ result = schema.execute(query) assert not result.errors assert result.data["events"]["edges"] == [ {"node": {"name": "Musical"}}, {"node": {"name": "Ballet"}}, ] query = """ query { events (tags_Len: 0) { edges { node { name } } } } """ result = schema.execute(query) assert not result.errors assert result.data["events"]["edges"] == [ {"node": {"name": "Speech"}}, ] query = """ query { events (tags_Len: 10) { edges { node { name } } } } """ result = schema.execute(query) assert not result.errors assert result.data["events"]["edges"] == [] query = """ query { events (tags_Len: "2") { edges { node { name } } } } """ result = schema.execute(query) assert len(result.errors) == 1 assert result.errors[0].message == 'Int cannot represent non-integer value: "2"' query = """ query { events (tags_Len: True) { edges { node { name } } } } """ result = schema.execute(query) assert len(result.errors) == 1 assert result.errors[0].message == "Int cannot represent non-integer value: True" @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") def test_array_field_custom_filter(schema): query = """ query { events (tags_Len_In: 2) { edges { node { name } } } } """ result = schema.execute(query) assert not result.errors assert result.data["events"]["edges"] == [ {"node": {"name": "Ballet"}}, {"node": {"name": "Musical"}}, ] query = """ query { events (tags_Len_In: [0, 2]) { edges { node { name } } } } """ result = schema.execute(query) assert not result.errors assert result.data["events"]["edges"] == [ {"node": {"name": "Ballet"}}, {"node": {"name": "Musical"}}, {"node": {"name": "Speech"}}, ] query = """ query { events (tags_Len_In: [10]) { edges { node { name } } } } """ result = schema.execute(query) assert not result.errors assert result.data["events"]["edges"] == [] query = """ query { events (tags_Len_In: []) { edges { node { name } } } } """ result = schema.execute(query) assert not result.errors assert result.data["events"]["edges"] == [] query = """ query { events (tags_Len_In: "12") { edges { node { name } } } } """ result = schema.execute(query) assert len(result.errors) == 1 assert result.errors[0].message == 'Int cannot represent non-integer value: "12"' query = """ query { events (tags_Len_In: True) { edges { node { name } } } } """ result = schema.execute(query) assert len(result.errors) == 1 assert result.errors[0].message == "Int cannot represent non-integer value: True" django-graphene-3.2.3/graphene_django/filter/tests/test_array_field_exact_filter.py000066400000000000000000000056011476451357100307540ustar00rootroot00000000000000import pytest from ...compat import ArrayField, MissingType @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") def test_array_field_exact_no_match(schema): """ Test exact filter on a array field of string. """ query = """ query { events (tags: ["concert", "music"]) { edges { node { name } } } } """ result = schema.execute(query) assert not result.errors assert result.data["events"]["edges"] == [] @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") def test_array_field_exact_match(schema): """ Test exact filter on a array field of string. """ query = """ query { events (tags: ["movie", "music"]) { edges { node { name } } } } """ result = schema.execute(query) assert not result.errors assert result.data["events"]["edges"] == [ {"node": {"name": "Musical"}}, ] @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") def test_array_field_exact_empty_list(schema): """ Test exact filter on a array field of string. """ query = """ query { events (tags: []) { edges { node { name } } } } """ result = schema.execute(query) assert not result.errors assert result.data["events"]["edges"] == [ {"node": {"name": "Speech"}}, ] @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") def test_array_field_filter_schema_type(schema): """ Check that the type in the filter is an array field like on the object type. """ schema_str = str(schema) assert ( '''type EventType implements Node { """The ID of the object""" id: ID! name: String! tags: [String!]! tagIds: [Int!]! randomField: [Boolean!]! }''' in schema_str ) filters = { "offset": "Int", "before": "String", "after": "String", "first": "Int", "last": "Int", "name": "String", "name_Contains": "String", "tags_Contains": "[String!]", "tags_Overlap": "[String!]", "tags": "[String!]", "tags_Len": "Int", "tags_Len_In": "[Int]", "tagsIds_Contains": "[Int!]", "tagsIds_Overlap": "[Int!]", "tagsIds": "[Int!]", "randomField_Contains": "[Boolean!]", "randomField_Overlap": "[Boolean!]", "randomField": "[Boolean!]", } filters_str = ", ".join( [f"{filter_field}: {gql_type}" for filter_field, gql_type in filters.items()] ) assert ( f"type Query {{\n events({filters_str}): EventTypeConnection\n}}" in schema_str ) django-graphene-3.2.3/graphene_django/filter/tests/test_array_field_overlap_filter.py000066400000000000000000000033741476451357100313250ustar00rootroot00000000000000import pytest from ...compat import ArrayField, MissingType @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") def test_array_field_overlap_multiple(schema): """ Test overlap filter on a array field of string. """ query = """ query { events (tags_Overlap: ["concert", "music"]) { edges { node { name } } } } """ result = schema.execute(query) assert not result.errors assert result.data["events"]["edges"] == [ {"node": {"name": "Live Show"}}, {"node": {"name": "Musical"}}, {"node": {"name": "Ballet"}}, ] @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") def test_array_field_overlap_one(schema): """ Test overlap filter on a array field of string. """ query = """ query { events (tags_Overlap: ["music"]) { edges { node { name } } } } """ result = schema.execute(query) assert not result.errors assert result.data["events"]["edges"] == [ {"node": {"name": "Live Show"}}, {"node": {"name": "Musical"}}, ] @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") def test_array_field_overlap_empty_list(schema): """ Test overlap filter on a array field of string. """ query = """ query { events (tags_Overlap: []) { edges { node { name } } } } """ result = schema.execute(query) assert not result.errors assert result.data["events"]["edges"] == [] django-graphene-3.2.3/graphene_django/filter/tests/test_enum_filtering.py000066400000000000000000000102201476451357100267420ustar00rootroot00000000000000import pytest import graphene from graphene.relay import Node from graphene_django import DjangoConnectionField, DjangoObjectType from graphene_django.tests.models import Article, Reporter from graphene_django.utils import DJANGO_FILTER_INSTALLED pytestmark = [] if DJANGO_FILTER_INSTALLED: from graphene_django.filter import DjangoFilterConnectionField else: pytestmark.append( pytest.mark.skipif( True, reason="django_filters not installed or not compatible" ) ) @pytest.fixture def schema(): class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" class ArticleType(DjangoObjectType): class Meta: model = Article interfaces = (Node,) fields = "__all__" filter_fields = { "lang": ["exact", "in"], "reporter__a_choice": ["exact", "in"], } class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) all_articles = DjangoFilterConnectionField(ArticleType) schema = graphene.Schema(query=Query) return schema @pytest.fixture def reporter_article_data(): john = Reporter.objects.create( first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 ) jane = Reporter.objects.create( first_name="Jane", last_name="Doe", email="janedoe@example.com", a_choice=2 ) Article.objects.create( headline="Article Node 1", reporter=john, editor=john, lang="es" ) Article.objects.create( headline="Article Node 2", reporter=john, editor=john, lang="en" ) Article.objects.create( headline="Article Node 3", reporter=jane, editor=jane, lang="en" ) def test_filter_enum_on_connection(schema, reporter_article_data): """ Check that we can filter with enums on a connection. """ query = """ query { allArticles(lang: ES) { edges { node { headline } } } } """ expected = { "allArticles": { "edges": [ {"node": {"headline": "Article Node 1"}}, ] } } result = schema.execute(query) assert not result.errors assert result.data == expected def test_filter_on_foreign_key_enum_field(schema, reporter_article_data): """ Check that we can filter with enums on a field from a foreign key. """ query = """ query { allArticles(reporter_AChoice: A_1) { edges { node { headline } } } } """ expected = { "allArticles": { "edges": [ {"node": {"headline": "Article Node 1"}}, {"node": {"headline": "Article Node 2"}}, ] } } result = schema.execute(query) assert not result.errors assert result.data == expected def test_filter_enum_field_schema_type(schema): """ Check that the type in the filter is an enum like on the object type. """ schema_str = str(schema) assert ( '''type ArticleType implements Node { """The ID of the object""" id: ID! headline: String! pubDate: Date! pubDateTime: DateTime! reporter: ReporterType! editor: ReporterType! """Language""" lang: TestsArticleLangChoices! importance: TestsArticleImportanceChoices }''' in schema_str ) filters = { "offset": "Int", "before": "String", "after": "String", "first": "Int", "last": "Int", "lang": "TestsArticleLangChoices", "lang_In": "[TestsArticleLangChoices]", "reporter_AChoice": "TestsReporterAChoiceChoices", "reporter_AChoice_In": "[TestsReporterAChoiceChoices]", } filters_str = ", ".join( [f"{filter_field}: {gql_type}" for filter_field, gql_type in filters.items()] ) assert f" allArticles({filters_str}): ArticleTypeConnection\n" in schema_str django-graphene-3.2.3/graphene_django/filter/tests/test_fields.py000066400000000000000000001107651476451357100252200ustar00rootroot00000000000000from datetime import datetime from textwrap import dedent import pytest from django.db.models import TextField, Value from django.db.models.functions import Concat from graphene import Argument, Boolean, Decimal, Field, ObjectType, Schema, String from graphene.relay import Node from graphene_django import DjangoObjectType from graphene_django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField from graphene_django.tests.models import Article, Person, Pet, Reporter from graphene_django.utils import DJANGO_FILTER_INSTALLED pytestmark = [] if DJANGO_FILTER_INSTALLED: import django_filters from django_filters import FilterSet, NumberFilter, OrderingFilter from graphene_django.filter import ( DjangoFilterConnectionField, GlobalIDFilter, GlobalIDMultipleChoiceFilter, ) from graphene_django.filter.tests.filters import ( ArticleFilter, PetFilter, ReporterFilter, ) else: pytestmark.append( pytest.mark.skipif( True, reason="django_filters not installed or not compatible" ) ) if DJANGO_FILTER_INSTALLED: class ArticleNode(DjangoObjectType): class Meta: model = Article interfaces = (Node,) fields = "__all__" filter_fields = ("headline",) class ReporterNode(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" class PetNode(DjangoObjectType): class Meta: model = Pet interfaces = (Node,) fields = "__all__" def get_args(field): return field.args def assert_arguments(field, *arguments): ignore = ("offset", "after", "before", "first", "last", "order_by") args = get_args(field) actual = [name for name in args if name not in ignore and not name.startswith("_")] assert set(arguments) == set( actual ), f"Expected arguments ({arguments}) did not match actual ({actual})" def assert_orderable(field): args = get_args(field) assert "order_by" in args, "Field cannot be ordered" def assert_not_orderable(field): args = get_args(field) assert "order_by" not in args, "Field can be ordered" def test_filter_explicit_filterset_arguments(): field = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleFilter) assert_arguments( field, "headline", "headline__icontains", "pub_date", "pub_date__gt", "pub_date__lt", "reporter", "reporter__in", ) def test_filter_shortcut_filterset_arguments_list(): field = DjangoFilterConnectionField(ArticleNode, fields=["pub_date", "reporter"]) assert_arguments(field, "pub_date", "reporter") def test_filter_shortcut_filterset_arguments_dict(): field = DjangoFilterConnectionField( ArticleNode, fields={"headline": ["exact", "icontains"], "reporter": ["exact"]} ) assert_arguments(field, "headline", "headline__icontains", "reporter") def test_filter_explicit_filterset_orderable(): field = DjangoFilterConnectionField(ReporterNode, filterset_class=ReporterFilter) assert_orderable(field) # def test_filter_shortcut_filterset_orderable_true(): # field = DjangoFilterConnectionField(ReporterNode) # assert_not_orderable(field) # def test_filter_shortcut_filterset_orderable_headline(): # field = DjangoFilterConnectionField(ReporterNode, order_by=['headline']) # assert_orderable(field) def test_filter_explicit_filterset_not_orderable(): field = DjangoFilterConnectionField(PetNode, filterset_class=PetFilter) assert_not_orderable(field) def test_filter_shortcut_filterset_extra_meta(): field = DjangoFilterConnectionField( ArticleNode, extra_filter_meta={"exclude": ("headline",)} ) assert "headline" not in field.filterset_class.get_fields() def test_filter_shortcut_filterset_context(): class ArticleContextFilter(django_filters.FilterSet): class Meta: model = Article exclude = set() @property def qs(self): qs = super().qs return qs.filter(reporter=self.request.reporter) class Query(ObjectType): context_articles = DjangoFilterConnectionField( ArticleNode, filterset_class=ArticleContextFilter ) r1 = Reporter.objects.create(first_name="r1", last_name="r1", email="r1@test.com") r2 = Reporter.objects.create(first_name="r2", last_name="r2", email="r2@test.com") Article.objects.create( headline="a1", pub_date=datetime.now(), pub_date_time=datetime.now(), reporter=r1, editor=r1, ) Article.objects.create( headline="a2", pub_date=datetime.now(), pub_date_time=datetime.now(), reporter=r2, editor=r2, ) class context: reporter = r2 query = """ query { contextArticles { edges { node { headline } } } } """ schema = Schema(query=Query) result = schema.execute(query, context_value=context()) assert not result.errors assert len(result.data["contextArticles"]["edges"]) == 1 assert result.data["contextArticles"]["edges"][0]["node"]["headline"] == "a2" def test_filter_filterset_information_on_meta(): class ReporterFilterNode(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" filter_fields = ["first_name", "articles"] field = DjangoFilterConnectionField(ReporterFilterNode) assert_arguments(field, "first_name", "articles") assert_not_orderable(field) def test_filter_filterset_information_on_meta_related(): class ReporterFilterNode(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" filter_fields = ["first_name", "articles"] class ArticleFilterNode(DjangoObjectType): class Meta: model = Article interfaces = (Node,) fields = "__all__" filter_fields = ["headline", "reporter"] class Query(ObjectType): all_reporters = DjangoFilterConnectionField(ReporterFilterNode) all_articles = DjangoFilterConnectionField(ArticleFilterNode) reporter = Field(ReporterFilterNode) article = Field(ArticleFilterNode) Schema(query=Query) articles_field = ReporterFilterNode._meta.fields["articles"].get_type() assert_arguments(articles_field, "headline", "reporter") assert_not_orderable(articles_field) def test_filter_filterset_class_filter_fields_exception(): with pytest.raises(Exception): class ReporterFilter(FilterSet): class Meta: model = Reporter fields = ["first_name", "articles"] class ReporterFilterNode(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" filterset_class = ReporterFilter filter_fields = ["first_name", "articles"] def test_filter_filterset_class_information_on_meta(): class ReporterFilter(FilterSet): class Meta: model = Reporter fields = ["first_name", "articles"] class ReporterFilterNode(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" filterset_class = ReporterFilter field = DjangoFilterConnectionField(ReporterFilterNode) assert_arguments(field, "first_name", "articles") assert_not_orderable(field) def test_filter_filterset_class_information_on_meta_related(): class ReporterFilter(FilterSet): class Meta: model = Reporter fields = ["first_name", "articles"] class ArticleFilter(FilterSet): class Meta: model = Article fields = ["headline", "reporter"] class ReporterFilterNode(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" filterset_class = ReporterFilter class ArticleFilterNode(DjangoObjectType): class Meta: model = Article interfaces = (Node,) fields = "__all__" filterset_class = ArticleFilter class Query(ObjectType): all_reporters = DjangoFilterConnectionField(ReporterFilterNode) all_articles = DjangoFilterConnectionField(ArticleFilterNode) reporter = Field(ReporterFilterNode) article = Field(ArticleFilterNode) Schema(query=Query) articles_field = ReporterFilterNode._meta.fields["articles"].get_type() assert_arguments(articles_field, "headline", "reporter") assert_not_orderable(articles_field) def test_filter_filterset_related_results(): class ReporterFilterNode(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" filter_fields = ["first_name", "articles"] class ArticleFilterNode(DjangoObjectType): class Meta: interfaces = (Node,) model = Article fields = "__all__" filter_fields = ["headline", "reporter"] class Query(ObjectType): all_reporters = DjangoFilterConnectionField(ReporterFilterNode) all_articles = DjangoFilterConnectionField(ArticleFilterNode) reporter = Field(ReporterFilterNode) article = Field(ArticleFilterNode) r1 = Reporter.objects.create(first_name="r1", last_name="r1", email="r1@test.com") r2 = Reporter.objects.create(first_name="r2", last_name="r2", email="r2@test.com") Article.objects.create( headline="a1", pub_date=datetime.now(), pub_date_time=datetime.now(), reporter=r1, editor=r1, ) Article.objects.create( headline="a2", pub_date=datetime.now(), pub_date_time=datetime.now(), reporter=r2, editor=r2, ) query = """ query { allReporters { edges { node { articles { edges { node { headline } } } } } } } """ schema = Schema(query=Query) result = schema.execute(query) assert not result.errors # We should only get back a single article for each reporter assert ( len(result.data["allReporters"]["edges"][0]["node"]["articles"]["edges"]) == 1 ) assert ( len(result.data["allReporters"]["edges"][1]["node"]["articles"]["edges"]) == 1 ) def test_global_id_field_implicit(): field = DjangoFilterConnectionField(ArticleNode, fields=["id"]) filterset_class = field.filterset_class id_filter = filterset_class.base_filters["id"] assert isinstance(id_filter, GlobalIDFilter) assert id_filter.field_class == GlobalIDFormField def test_global_id_field_explicit(): class ArticleIdFilter(django_filters.FilterSet): class Meta: model = Article fields = ["id"] field = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleIdFilter) filterset_class = field.filterset_class id_filter = filterset_class.base_filters["id"] assert isinstance(id_filter, GlobalIDFilter) assert id_filter.field_class == GlobalIDFormField def test_filterset_descriptions(): class ArticleIdFilter(django_filters.FilterSet): class Meta: model = Article fields = ["id"] max_time = django_filters.NumberFilter( method="filter_max_time", label="The maximum time" ) field = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleIdFilter) max_time = field.args["max_time"] assert isinstance(max_time, Argument) assert max_time.type == Decimal assert max_time.description == "The maximum time" def test_global_id_field_relation(): field = DjangoFilterConnectionField(ArticleNode, fields=["reporter"]) filterset_class = field.filterset_class id_filter = filterset_class.base_filters["reporter"] assert isinstance(id_filter, GlobalIDFilter) assert id_filter.field_class == GlobalIDFormField def test_global_id_field_relation_with_filter(): class ReporterFilterNode(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" filter_fields = ["first_name", "articles"] class ArticleFilterNode(DjangoObjectType): class Meta: model = Article interfaces = (Node,) fields = "__all__" filter_fields = ["headline", "reporter"] class Query(ObjectType): all_reporters = DjangoFilterConnectionField(ReporterFilterNode) all_articles = DjangoFilterConnectionField(ArticleFilterNode) reporter = Field(ReporterFilterNode) article = Field(ArticleFilterNode) r1 = Reporter.objects.create(first_name="r1", last_name="r1", email="r1@test.com") r2 = Reporter.objects.create(first_name="r2", last_name="r2", email="r2@test.com") Article.objects.create( headline="a1", pub_date=datetime.now(), pub_date_time=datetime.now(), reporter=r1, editor=r1, ) Article.objects.create( headline="a2", pub_date=datetime.now(), pub_date_time=datetime.now(), reporter=r2, editor=r2, ) # Query articles created by the reporter `r1` query = """ query { allArticles (reporter: "UmVwb3J0ZXJGaWx0ZXJOb2RlOjE=") { edges { node { id } } } } """ schema = Schema(query=Query) result = schema.execute(query) assert not result.errors # We should only get back a single article assert len(result.data["allArticles"]["edges"]) == 1 def test_global_id_field_relation_with_filter_not_valid_id(): class ReporterFilterNode(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" filter_fields = ["first_name", "articles"] class ArticleFilterNode(DjangoObjectType): class Meta: model = Article interfaces = (Node,) fields = "__all__" filter_fields = ["headline", "reporter"] class Query(ObjectType): all_reporters = DjangoFilterConnectionField(ReporterFilterNode) all_articles = DjangoFilterConnectionField(ArticleFilterNode) reporter = Field(ReporterFilterNode) article = Field(ArticleFilterNode) r1 = Reporter.objects.create(first_name="r1", last_name="r1", email="r1@test.com") r2 = Reporter.objects.create(first_name="r2", last_name="r2", email="r2@test.com") Article.objects.create( headline="a1", pub_date=datetime.now(), pub_date_time=datetime.now(), reporter=r1, editor=r1, ) Article.objects.create( headline="a2", pub_date=datetime.now(), pub_date_time=datetime.now(), reporter=r2, editor=r2, ) # Filter by the global ID that does not exist query = """ query { allArticles (reporter: "fake_global_id") { edges { node { id } } } } """ schema = Schema(query=Query) result = schema.execute(query) assert "Invalid ID specified." in result.errors[0].message def test_global_id_multiple_field_implicit(): field = DjangoFilterConnectionField(ReporterNode, fields=["pets"]) filterset_class = field.filterset_class multiple_filter = filterset_class.base_filters["pets"] assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter) assert multiple_filter.field_class == GlobalIDMultipleChoiceField def test_global_id_multiple_field_explicit(): class ReporterPetsFilter(django_filters.FilterSet): class Meta: model = Reporter fields = ["pets"] field = DjangoFilterConnectionField( ReporterNode, filterset_class=ReporterPetsFilter ) filterset_class = field.filterset_class multiple_filter = filterset_class.base_filters["pets"] assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter) assert multiple_filter.field_class == GlobalIDMultipleChoiceField def test_global_id_multiple_field_implicit_reverse(): field = DjangoFilterConnectionField(ReporterNode, fields=["articles"]) filterset_class = field.filterset_class multiple_filter = filterset_class.base_filters["articles"] assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter) assert multiple_filter.field_class == GlobalIDMultipleChoiceField def test_global_id_multiple_field_explicit_reverse(): class ReporterPetsFilter(django_filters.FilterSet): class Meta: model = Reporter fields = ["articles"] field = DjangoFilterConnectionField( ReporterNode, filterset_class=ReporterPetsFilter ) filterset_class = field.filterset_class multiple_filter = filterset_class.base_filters["articles"] assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter) assert multiple_filter.field_class == GlobalIDMultipleChoiceField def test_filter_filterset_related_results_with_filter(): class ReporterFilterNode(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" filter_fields = {"first_name": ["icontains"]} class Query(ObjectType): all_reporters = DjangoFilterConnectionField(ReporterFilterNode) Reporter.objects.create( first_name="A test user", last_name="Last Name", email="test1@test.com" ) Reporter.objects.create( first_name="Other test user", last_name="Other Last Name", email="test2@test.com", ) Reporter.objects.create( first_name="Random", last_name="RandomLast", email="random@test.com" ) query = """ query { allReporters(firstName_Icontains: "test") { edges { node { id } } } } """ schema = Schema(query=Query) result = schema.execute(query) assert not result.errors # We should only get two reporters assert len(result.data["allReporters"]["edges"]) == 2 def test_recursive_filter_connection(): class ReporterFilterNode(DjangoObjectType): child_reporters = DjangoFilterConnectionField(lambda: ReporterFilterNode) def resolve_child_reporters(self, **args): return [] class Meta: model = Reporter interfaces = (Node,) fields = "__all__" class Query(ObjectType): all_reporters = DjangoFilterConnectionField(ReporterFilterNode) assert ( ReporterFilterNode._meta.fields["child_reporters"].node_type == ReporterFilterNode ) def test_should_query_filter_node_limit(): class ReporterFilter(FilterSet): limit = NumberFilter(method="filter_limit") def filter_limit(self, queryset, name, value): return queryset[:value] class Meta: model = Reporter fields = ["first_name"] class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" class ArticleType(DjangoObjectType): class Meta: model = Article interfaces = (Node,) fields = "__all__" filter_fields = ("lang",) class Query(ObjectType): all_reporters = DjangoFilterConnectionField( ReporterType, filterset_class=ReporterFilter ) def resolve_all_reporters(self, info, **args): return Reporter.objects.order_by("a_choice") Reporter.objects.create( first_name="Bob", last_name="Doe", email="bobdoe@example.com", a_choice=2 ) r = Reporter.objects.create( first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 ) Article.objects.create( headline="Article Node 1", pub_date=datetime.now(), pub_date_time=datetime.now(), reporter=r, editor=r, lang="es", ) Article.objects.create( headline="Article Node 2", pub_date=datetime.now(), pub_date_time=datetime.now(), reporter=r, editor=r, lang="en", ) schema = Schema(query=Query) query = """ query NodeFilteringQuery { allReporters(limit: 1) { edges { node { id firstName articles(lang: ES) { edges { node { id lang } } } } } } } """ expected = { "allReporters": { "edges": [ { "node": { "id": "UmVwb3J0ZXJUeXBlOjI=", "firstName": "John", "articles": { "edges": [ {"node": {"id": "QXJ0aWNsZVR5cGU6MQ==", "lang": "ES"}} ] }, } } ] } } result = schema.execute(query) assert not result.errors assert result.data == expected def test_order_by(): class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" class Query(ObjectType): all_reporters = DjangoFilterConnectionField( ReporterType, filterset_class=ReporterFilter ) Reporter.objects.create(first_name="b") Reporter.objects.create(first_name="a") schema = Schema(query=Query) query = """ query NodeFilteringQuery { allReporters(orderBy: "-firstName") { edges { node { firstName } } } } """ expected = { "allReporters": { "edges": [{"node": {"firstName": "b"}}, {"node": {"firstName": "a"}}] } } result = schema.execute(query) assert not result.errors assert result.data == expected query = """ query NodeFilteringQuery { allReporters(orderBy: "-first_name") { edges { node { firstName } } } } """ result = schema.execute(query) assert not result.errors assert result.data == expected query = """ query NodeFilteringQuery { allReporters(orderBy: "-firstname") { edges { node { firstName } } } } """ result = schema.execute(query) assert result.errors def test_order_by_is_preserved(): class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" filter_fields = () class Query(ObjectType): all_reporters = DjangoFilterConnectionField( ReporterType, reverse_order=Boolean() ) def resolve_all_reporters(self, info, reverse_order=False, **args): reporters = Reporter.objects.order_by("first_name") if reverse_order: return reporters.reverse() return reporters Reporter.objects.create(first_name="b") Reporter.objects.create(first_name="a") schema = Schema(query=Query) query = """ query NodeFilteringQuery { allReporters(first: 1) { edges { node { firstName } } } } """ expected = {"allReporters": {"edges": [{"node": {"firstName": "a"}}]}} result = schema.execute(query) assert not result.errors assert result.data == expected reverse_query = """ query NodeFilteringQuery { allReporters(first: 1, reverseOrder: true) { edges { node { firstName } } } } """ reverse_expected = {"allReporters": {"edges": [{"node": {"firstName": "b"}}]}} reverse_result = schema.execute(reverse_query) assert not reverse_result.errors assert reverse_result.data == reverse_expected def test_annotation_is_preserved(): class ReporterType(DjangoObjectType): full_name = String() def resolve_full_name(instance, info, **args): return instance.full_name class Meta: model = Reporter interfaces = (Node,) fields = "__all__" filter_fields = () class Query(ObjectType): all_reporters = DjangoFilterConnectionField(ReporterType) def resolve_all_reporters(self, info, **args): return Reporter.objects.annotate( full_name=Concat( "first_name", Value(" "), "last_name", output_field=TextField() ) ) Reporter.objects.create(first_name="John", last_name="Doe") schema = Schema(query=Query) query = """ query NodeFilteringQuery { allReporters(first: 1) { edges { node { fullName } } } } """ expected = {"allReporters": {"edges": [{"node": {"fullName": "John Doe"}}]}} result = schema.execute(query) assert not result.errors assert result.data == expected def test_annotation_with_only(): class ReporterType(DjangoObjectType): full_name = String() class Meta: model = Reporter interfaces = (Node,) fields = "__all__" filter_fields = () class Query(ObjectType): all_reporters = DjangoFilterConnectionField(ReporterType) def resolve_all_reporters(self, info, **args): return Reporter.objects.only("first_name", "last_name").annotate( full_name=Concat( "first_name", Value(" "), "last_name", output_field=TextField() ) ) Reporter.objects.create(first_name="John", last_name="Doe") schema = Schema(query=Query) query = """ query NodeFilteringQuery { allReporters(first: 1) { edges { node { fullName } } } } """ expected = {"allReporters": {"edges": [{"node": {"fullName": "John Doe"}}]}} result = schema.execute(query) assert not result.errors assert result.data == expected def test_node_get_queryset_is_called(): class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" filter_fields = () @classmethod def get_queryset(cls, queryset, info): return queryset.filter(first_name="b") class Query(ObjectType): all_reporters = DjangoFilterConnectionField( ReporterType, reverse_order=Boolean() ) Reporter.objects.create(first_name="b") Reporter.objects.create(first_name="a") schema = Schema(query=Query) query = """ query NodeFilteringQuery { allReporters(first: 10) { edges { node { firstName } } } } """ expected = {"allReporters": {"edges": [{"node": {"firstName": "b"}}]}} result = schema.execute(query) assert not result.errors assert result.data == expected def test_integer_field_filter_type(): class PetType(DjangoObjectType): class Meta: model = Pet interfaces = (Node,) filter_fields = {"age": ["exact"]} fields = ("age",) class Query(ObjectType): pets = DjangoFilterConnectionField(PetType) schema = Schema(query=Query) assert str(schema) == dedent( """\ type Query { pets(offset: Int, before: String, after: String, first: Int, last: Int, age: Int): PetTypeConnection } type PetTypeConnection { \"""Pagination data for this connection.\""" pageInfo: PageInfo! \"""Contains the nodes in this connection.\""" edges: [PetTypeEdge]! } \""" The Relay compliant `PageInfo` type, containing data necessary to paginate this connection. \""" type PageInfo { \"""When paginating forwards, are there more items?\""" hasNextPage: Boolean! \"""When paginating backwards, are there more items?\""" hasPreviousPage: Boolean! \"""When paginating backwards, the cursor to continue.\""" startCursor: String \"""When paginating forwards, the cursor to continue.\""" endCursor: String } \"""A Relay edge containing a `PetType` and its cursor.\""" type PetTypeEdge { \"""The item at the end of the edge\""" node: PetType \"""A cursor for use in pagination\""" cursor: String! } type PetType implements Node { age: Int! \"""The ID of the object\""" id: ID! } \"""An object with an ID\""" interface Node { \"""The ID of the object\""" id: ID! }""" ) def test_other_filter_types(): class PetType(DjangoObjectType): class Meta: model = Pet interfaces = (Node,) filter_fields = {"age": ["exact", "isnull", "lt"]} fields = ("age",) class Query(ObjectType): pets = DjangoFilterConnectionField(PetType) schema = Schema(query=Query) assert str(schema) == dedent( """\ type Query { pets(offset: Int, before: String, after: String, first: Int, last: Int, age: Int, age_Isnull: Boolean, age_Lt: Int): PetTypeConnection } type PetTypeConnection { \"""Pagination data for this connection.\""" pageInfo: PageInfo! \"""Contains the nodes in this connection.\""" edges: [PetTypeEdge]! } \""" The Relay compliant `PageInfo` type, containing data necessary to paginate this connection. \""" type PageInfo { \"""When paginating forwards, are there more items?\""" hasNextPage: Boolean! \"""When paginating backwards, are there more items?\""" hasPreviousPage: Boolean! \"""When paginating backwards, the cursor to continue.\""" startCursor: String \"""When paginating forwards, the cursor to continue.\""" endCursor: String } \"""A Relay edge containing a `PetType` and its cursor.\""" type PetTypeEdge { \"""The item at the end of the edge\""" node: PetType \"""A cursor for use in pagination\""" cursor: String! } type PetType implements Node { age: Int! \"""The ID of the object\""" id: ID! } \"""An object with an ID\""" interface Node { \"""The ID of the object\""" id: ID! }""" ) def test_filter_filterset_based_on_mixin(): class ArticleFilterMixin(FilterSet): @classmethod def get_filters(cls): filters = super(FilterSet, cls).get_filters() filters.update( { "viewer__email__in": django_filters.CharFilter( method="filter_email_in", field_name="reporter__email__in" ) } ) return filters def filter_email_in(self, queryset, name, value): return queryset.filter(**{name: [value]}) class NewArticleFilter(ArticleFilterMixin, ArticleFilter): pass class NewReporterNode(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" class NewArticleFilterNode(DjangoObjectType): viewer = Field(NewReporterNode) class Meta: model = Article interfaces = (Node,) fields = "__all__" filterset_class = NewArticleFilter def resolve_viewer(self, info): return self.reporter class Query(ObjectType): all_articles = DjangoFilterConnectionField(NewArticleFilterNode) reporter_1 = Reporter.objects.create( first_name="John", last_name="Doe", email="john@doe.com" ) article_1 = Article.objects.create( headline="Hello", reporter=reporter_1, editor=reporter_1, pub_date=datetime.now(), pub_date_time=datetime.now(), ) reporter_2 = Reporter.objects.create( first_name="Adam", last_name="Doe", email="adam@doe.com" ) Article.objects.create( headline="Good Bye", reporter=reporter_2, editor=reporter_2, pub_date=datetime.now(), pub_date_time=datetime.now(), ) schema = Schema(query=Query) query = """ query NodeFilteringQuery ($email: String!) { allArticles(viewer_Email_In: $email) { edges { node { headline viewer { email } } } } } """ expected = { "allArticles": { "edges": [ { "node": { "headline": article_1.headline, "viewer": {"email": reporter_1.email}, } } ] } } result = schema.execute(query, variable_values={"email": reporter_1.email}) assert not result.errors assert result.data == expected def test_filter_string_contains(): class PersonType(DjangoObjectType): class Meta: model = Person interfaces = (Node,) fields = "__all__" filter_fields = {"name": ["exact", "in", "contains", "icontains"]} class Query(ObjectType): people = DjangoFilterConnectionField(PersonType) schema = Schema(query=Query) Person.objects.bulk_create( [ Person(name="Jack"), Person(name="Joe"), Person(name="Jane"), Person(name="Peter"), Person(name="Bob"), ] ) query = """query nameContain($filter: String) { people(name_Contains: $filter) { edges { node { name } } } }""" result = schema.execute(query, variables={"filter": "Ja"}) assert not result.errors assert result.data == { "people": { "edges": [ {"node": {"name": "Jack"}}, {"node": {"name": "Jane"}}, ] } } result = schema.execute(query, variables={"filter": "o"}) assert not result.errors assert result.data == { "people": { "edges": [ {"node": {"name": "Joe"}}, {"node": {"name": "Bob"}}, ] } } def test_only_custom_filters(): class ReporterFilter(FilterSet): class Meta: model = Reporter fields = [] some_filter = OrderingFilter(fields=("name",)) class ReporterFilterNode(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" filterset_class = ReporterFilter field = DjangoFilterConnectionField(ReporterFilterNode) assert_arguments(field, "some_filter") django-graphene-3.2.3/graphene_django/filter/tests/test_in_filter.py000066400000000000000000000266021476451357100257210ustar00rootroot00000000000000from datetime import datetime import pytest from django_filters import ( FilterSet, rest_framework as filters, ) from graphene import ObjectType, Schema from graphene.relay import Node from graphene_django import DjangoObjectType from graphene_django.filter.tests.filters import ArticleFilter from graphene_django.tests.models import Article, Film, Person, Pet, Reporter from graphene_django.utils import DJANGO_FILTER_INSTALLED pytestmark = [] if DJANGO_FILTER_INSTALLED: from graphene_django.filter import DjangoFilterConnectionField else: pytestmark.append( pytest.mark.skipif( True, reason="django_filters not installed or not compatible" ) ) @pytest.fixture def query(): class PetNode(DjangoObjectType): class Meta: model = Pet interfaces = (Node,) fields = "__all__" filter_fields = { "id": ["exact", "in"], "name": ["exact", "in"], "age": ["exact", "in", "range"], } class ReporterNode(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" # choice filter using enum filter_fields = {"reporter_type": ["exact", "in"]} class ArticleNode(DjangoObjectType): class Meta: model = Article interfaces = (Node,) fields = "__all__" filterset_class = ArticleFilter class FilmNode(DjangoObjectType): class Meta: model = Film interfaces = (Node,) fields = "__all__" # choice filter not using enum filter_fields = { "genre": ["exact", "in"], } convert_choices_to_enum = False class PersonFilterSet(FilterSet): class Meta: model = Person fields = {"name": ["in"]} names = filters.BaseInFilter(method="filter_names") def filter_names(self, qs, name, value): """ This custom filter take a string as input with comma separated values. Note that the value here is already a list as it has been transformed by the BaseInFilter class. """ return qs.filter(name__in=value) class PersonNode(DjangoObjectType): class Meta: model = Person interfaces = (Node,) filterset_class = PersonFilterSet fields = "__all__" class Query(ObjectType): pets = DjangoFilterConnectionField(PetNode) people = DjangoFilterConnectionField(PersonNode) articles = DjangoFilterConnectionField(ArticleNode) films = DjangoFilterConnectionField(FilmNode) reporters = DjangoFilterConnectionField(ReporterNode) return Query def test_string_in_filter(query): """ Test in filter on a string field. """ Pet.objects.create(name="Brutus", age=12) Pet.objects.create(name="Mimi", age=3) Pet.objects.create(name="Jojo, the rabbit", age=3) schema = Schema(query=query) query = """ query { pets (name_In: ["Brutus", "Jojo, the rabbit"]) { edges { node { name } } } } """ result = schema.execute(query) assert not result.errors assert result.data["pets"]["edges"] == [ {"node": {"name": "Brutus"}}, {"node": {"name": "Jojo, the rabbit"}}, ] def test_string_in_filter_with_otjer_filter(query): """ Test in filter on a string field which has also a custom filter doing a similar operation. """ Person.objects.create(name="John") Person.objects.create(name="Michael") Person.objects.create(name="Angela") schema = Schema(query=query) query = """ query { people (name_In: ["John", "Michael"]) { edges { node { name } } } } """ result = schema.execute(query) assert not result.errors assert result.data["people"]["edges"] == [ {"node": {"name": "John"}}, {"node": {"name": "Michael"}}, ] def test_string_in_filter_with_declared_filter(query): """ Test in filter on a string field with a custom filterset class. """ Person.objects.create(name="John") Person.objects.create(name="Michael") Person.objects.create(name="Angela") schema = Schema(query=query) query = """ query { people (names: "John,Michael") { edges { node { name } } } } """ result = schema.execute(query) assert not result.errors assert result.data["people"]["edges"] == [ {"node": {"name": "John"}}, {"node": {"name": "Michael"}}, ] def test_int_in_filter(query): """ Test in filter on an integer field. """ Pet.objects.create(name="Brutus", age=12) Pet.objects.create(name="Mimi", age=3) Pet.objects.create(name="Jojo, the rabbit", age=3) schema = Schema(query=query) query = """ query { pets (age_In: [3]) { edges { node { name } } } } """ result = schema.execute(query) assert not result.errors assert result.data["pets"]["edges"] == [ {"node": {"name": "Mimi"}}, {"node": {"name": "Jojo, the rabbit"}}, ] query = """ query { pets (age_In: [3, 12]) { edges { node { name } } } } """ result = schema.execute(query) assert not result.errors assert result.data["pets"]["edges"] == [ {"node": {"name": "Brutus"}}, {"node": {"name": "Mimi"}}, {"node": {"name": "Jojo, the rabbit"}}, ] def test_in_filter_with_empty_list(query): """ Check that using a in filter with an empty list provided as input returns no objects. """ Pet.objects.create(name="Brutus", age=12) Pet.objects.create(name="Mimi", age=8) Pet.objects.create(name="Picotin", age=5) schema = Schema(query=query) query = """ query { pets (name_In: []) { edges { node { name } } } } """ result = schema.execute(query) assert not result.errors assert len(result.data["pets"]["edges"]) == 0 def test_choice_in_filter_without_enum(query): """ Test in filter o an choice field not using an enum (Film.genre). """ john_doe = Reporter.objects.create( first_name="John", last_name="Doe", email="john@doe.com" ) jean_bon = Reporter.objects.create( first_name="Jean", last_name="Bon", email="jean@bon.com" ) documentary_film = Film.objects.create(genre="do") documentary_film.reporters.add(john_doe) action_film = Film.objects.create(genre="ac") action_film.reporters.add(john_doe) other_film = Film.objects.create(genre="ot") other_film.reporters.add(john_doe) other_film.reporters.add(jean_bon) schema = Schema(query=query) query = """ query { films (genre_In: ["do", "ac"]) { edges { node { genre reporters { edges { node { lastName } } } } } } } """ result = schema.execute(query) assert not result.errors assert result.data["films"]["edges"] == [ { "node": { "genre": "do", "reporters": {"edges": [{"node": {"lastName": "Doe"}}]}, } }, { "node": { "genre": "ac", "reporters": {"edges": [{"node": {"lastName": "Doe"}}]}, } }, ] def test_fk_id_in_filter(query): """ Test in filter on an foreign key relationship. """ john_doe = Reporter.objects.create( first_name="John", last_name="Doe", email="john@doe.com" ) jean_bon = Reporter.objects.create( first_name="Jean", last_name="Bon", email="jean@bon.com" ) sara_croche = Reporter.objects.create( first_name="Sara", last_name="Croche", email="sara@croche.com" ) Article.objects.create( headline="A", pub_date=datetime.now(), pub_date_time=datetime.now(), reporter=john_doe, editor=john_doe, ) Article.objects.create( headline="B", pub_date=datetime.now(), pub_date_time=datetime.now(), reporter=jean_bon, editor=jean_bon, ) Article.objects.create( headline="C", pub_date=datetime.now(), pub_date_time=datetime.now(), reporter=sara_croche, editor=sara_croche, ) schema = Schema(query=query) query = f""" query {{ articles (reporter_In: [{john_doe.id}, {jean_bon.id}]) {{ edges {{ node {{ headline reporter {{ lastName }} }} }} }} }} """ result = schema.execute(query) assert not result.errors assert result.data["articles"]["edges"] == [ {"node": {"headline": "A", "reporter": {"lastName": "Doe"}}}, {"node": {"headline": "B", "reporter": {"lastName": "Bon"}}}, ] def test_enum_in_filter(query): """ Test in filter on a choice field using an enum (Reporter.reporter_type). """ Reporter.objects.create( first_name="John", last_name="Doe", email="john@doe.com", reporter_type=1 ) Reporter.objects.create( first_name="Jean", last_name="Bon", email="jean@bon.com", reporter_type=2 ) Reporter.objects.create( first_name="Jane", last_name="Doe", email="jane@doe.com", reporter_type=2 ) Reporter.objects.create( first_name="Jack", last_name="Black", email="jack@black.com", reporter_type=None ) schema = Schema(query=query) query = """ query { reporters (reporterType_In: [A_1]) { edges { node { email } } } } """ result = schema.execute(query) assert not result.errors assert result.data["reporters"]["edges"] == [ {"node": {"email": "john@doe.com"}}, ] query = """ query { reporters (reporterType_In: [A_2]) { edges { node { email } } } } """ result = schema.execute(query) assert not result.errors assert result.data["reporters"]["edges"] == [ {"node": {"email": "jean@bon.com"}}, {"node": {"email": "jane@doe.com"}}, ] query = """ query { reporters (reporterType_In: [A_2, A_1]) { edges { node { email } } } } """ result = schema.execute(query) assert not result.errors assert result.data["reporters"]["edges"] == [ {"node": {"email": "john@doe.com"}}, {"node": {"email": "jean@bon.com"}}, {"node": {"email": "jane@doe.com"}}, ] django-graphene-3.2.3/graphene_django/filter/tests/test_range_filter.py000066400000000000000000000055321476451357100264060ustar00rootroot00000000000000import json import pytest from graphene import ObjectType, Schema from graphene.relay import Node from graphene_django import DjangoObjectType from graphene_django.tests.models import Pet from graphene_django.utils import DJANGO_FILTER_INSTALLED pytestmark = [] if DJANGO_FILTER_INSTALLED: from graphene_django.filter import DjangoFilterConnectionField else: pytestmark.append( pytest.mark.skipif( True, reason="django_filters not installed or not compatible" ) ) class PetNode(DjangoObjectType): class Meta: model = Pet interfaces = (Node,) fields = "__all__" filter_fields = { "name": ["exact", "in"], "age": ["exact", "in", "range"], } class Query(ObjectType): pets = DjangoFilterConnectionField(PetNode) def test_int_range_filter(): """ Test range filter on an integer field. """ Pet.objects.create(name="Brutus", age=12) Pet.objects.create(name="Mimi", age=8) Pet.objects.create(name="Jojo, the rabbit", age=3) Pet.objects.create(name="Picotin", age=5) schema = Schema(query=Query) query = """ query { pets (age_Range: [4, 9]) { edges { node { name } } } } """ result = schema.execute(query) assert not result.errors assert result.data["pets"]["edges"] == [ {"node": {"name": "Mimi"}}, {"node": {"name": "Picotin"}}, ] def test_range_filter_with_invalid_input(): """ Test range filter used with invalid inputs raise an error. """ Pet.objects.create(name="Brutus", age=12) Pet.objects.create(name="Mimi", age=8) Pet.objects.create(name="Jojo, the rabbit", age=3) Pet.objects.create(name="Picotin", age=5) schema = Schema(query=Query) query = """ query ($rangeValue: [Int]) { pets (age_Range: $rangeValue) { edges { node { name } } } } """ expected_error = json.dumps( { "age__range": [ { "message": "Invalid range specified: it needs to contain 2 values.", "code": "invalid", } ] } ) # Empty list result = schema.execute(query, variables={"rangeValue": []}) assert len(result.errors) == 1 assert result.errors[0].message == expected_error # Only one item in the list result = schema.execute(query, variables={"rangeValue": [1]}) assert len(result.errors) == 1 assert result.errors[0].message == expected_error # More than 2 items in the list result = schema.execute(query, variables={"rangeValue": [1, 2, 3]}) assert len(result.errors) == 1 assert result.errors[0].message == expected_error django-graphene-3.2.3/graphene_django/filter/tests/test_typed_filter.py000066400000000000000000000162671476451357100264460ustar00rootroot00000000000000import operator from functools import reduce import pytest from django.db.models import Q from django_filters import FilterSet import graphene from graphene.relay import Node from graphene_django import DjangoObjectType from graphene_django.tests.models import Article, Reporter from graphene_django.utils import DJANGO_FILTER_INSTALLED pytestmark = [] if DJANGO_FILTER_INSTALLED: from graphene_django.filter import ( DjangoFilterConnectionField, ListFilter, TypedFilter, ) else: pytestmark.append( pytest.mark.skipif( True, reason="django_filters not installed or not compatible" ) ) @pytest.fixture def schema(): class ArticleFilterSet(FilterSet): class Meta: model = Article fields = { "lang": ["exact", "in"], } lang__contains = TypedFilter( field_name="lang", lookup_expr="icontains", input_type=graphene.String ) lang__in_str = ListFilter( field_name="lang", lookup_expr="in", input_type=graphene.List(graphene.String), ) first_n = TypedFilter(input_type=graphene.Int, method="first_n_filter") only_first = TypedFilter( input_type=graphene.Boolean, method="only_first_filter" ) headline_search = ListFilter( method="headline_search_filter", input_type=graphene.List(graphene.String), ) def first_n_filter(self, queryset, _name, value): return queryset[:value] def only_first_filter(self, queryset, _name, value): if value: return queryset[:1] else: return queryset def headline_search_filter(self, queryset, _name, value): if not value: return queryset.none() return queryset.filter( reduce(operator.or_, [Q(headline__icontains=v) for v in value]) ) class ArticleType(DjangoObjectType): class Meta: model = Article interfaces = (Node,) fields = "__all__" filterset_class = ArticleFilterSet class Query(graphene.ObjectType): articles = DjangoFilterConnectionField(ArticleType) schema = graphene.Schema(query=Query) return schema def test_typed_filter_schema(schema): """ Check that the type provided in the filter is reflected in the schema. """ schema_str = str(schema) filters = { "offset": "Int", "before": "String", "after": "String", "first": "Int", "last": "Int", "lang": "TestsArticleLangChoices", "lang_In": "[TestsArticleLangChoices]", "lang_Contains": "String", "lang_InStr": "[String]", "firstN": "Int", "onlyFirst": "Boolean", "headlineSearch": "[String]", } all_articles_filters = ( schema_str.split(" articles(")[1] .split("): ArticleTypeConnection\n")[0] .split(", ") ) for filter_field, gql_type in filters.items(): assert f"{filter_field}: {gql_type}" in all_articles_filters def test_typed_filters_work(schema): reporter = Reporter.objects.create(first_name="John", last_name="Doe", email="") Article.objects.create(headline="A", reporter=reporter, editor=reporter, lang="es") Article.objects.create(headline="B", reporter=reporter, editor=reporter, lang="es") Article.objects.create(headline="C", reporter=reporter, editor=reporter, lang="en") Article.objects.create(headline="AB", reporter=reporter, editor=reporter, lang="es") query = 'query { articles (lang_Contains: "n") { edges { node { headline } } } }' result = schema.execute(query) assert not result.errors assert result.data["articles"]["edges"] == [ {"node": {"headline": "C"}}, ] query = "query { articles (firstN: 2) { edges { node { headline } } } }" result = schema.execute(query) assert not result.errors assert result.data["articles"]["edges"] == [ {"node": {"headline": "A"}}, {"node": {"headline": "AB"}}, ] query = "query { articles (onlyFirst: true) { edges { node { headline } } } }" result = schema.execute(query) assert not result.errors assert result.data["articles"]["edges"] == [ {"node": {"headline": "A"}}, ] def test_list_filters_work(schema): reporter = Reporter.objects.create(first_name="John", last_name="Doe", email="") Article.objects.create(headline="A", reporter=reporter, editor=reporter, lang="es") Article.objects.create(headline="B", reporter=reporter, editor=reporter, lang="es") Article.objects.create(headline="C", reporter=reporter, editor=reporter, lang="en") Article.objects.create(headline="AB", reporter=reporter, editor=reporter, lang="es") query = "query { articles (lang_In: [ES]) { edges { node { headline } } } }" result = schema.execute(query) assert not result.errors assert result.data["articles"]["edges"] == [ {"node": {"headline": "A"}}, {"node": {"headline": "AB"}}, {"node": {"headline": "B"}}, ] query = 'query { articles (lang_InStr: ["es"]) { edges { node { headline } } } }' result = schema.execute(query) assert not result.errors assert result.data["articles"]["edges"] == [ {"node": {"headline": "A"}}, {"node": {"headline": "AB"}}, {"node": {"headline": "B"}}, ] query = "query { articles (lang_InStr: []) { edges { node { headline } } } }" result = schema.execute(query) assert not result.errors assert result.data["articles"]["edges"] == [] query = "query { articles (lang_InStr: null) { edges { node { headline } } } }" result = schema.execute(query) assert not result.errors assert result.data["articles"]["edges"] == [ {"node": {"headline": "A"}}, {"node": {"headline": "AB"}}, {"node": {"headline": "B"}}, {"node": {"headline": "C"}}, ] query = 'query { articles (headlineSearch: ["a", "B"]) { edges { node { headline } } } }' result = schema.execute(query) assert not result.errors assert result.data["articles"]["edges"] == [ {"node": {"headline": "A"}}, {"node": {"headline": "AB"}}, {"node": {"headline": "B"}}, ] query = "query { articles (headlineSearch: []) { edges { node { headline } } } }" result = schema.execute(query) assert not result.errors assert result.data["articles"]["edges"] == [] query = "query { articles (headlineSearch: null) { edges { node { headline } } } }" result = schema.execute(query) assert not result.errors assert result.data["articles"]["edges"] == [ {"node": {"headline": "A"}}, {"node": {"headline": "AB"}}, {"node": {"headline": "B"}}, {"node": {"headline": "C"}}, ] query = 'query { articles (headlineSearch: [""]) { edges { node { headline } } } }' result = schema.execute(query) assert not result.errors assert result.data["articles"]["edges"] == [ {"node": {"headline": "A"}}, {"node": {"headline": "AB"}}, {"node": {"headline": "B"}}, {"node": {"headline": "C"}}, ] django-graphene-3.2.3/graphene_django/filter/utils.py000066400000000000000000000146061476451357100227060ustar00rootroot00000000000000from django import forms from django_filters.utils import get_model_field import graphene from ..forms import GlobalIDFormField, GlobalIDMultipleChoiceField from .filters import ListFilter, RangeFilter, TypedFilter from .filterset import custom_filterset_factory, setup_filterset def get_field_type(registry, model, field_name): """ Try to get a model field corresponding Graphql type from the DjangoObjectType. """ object_type = registry.get_type_for_model(model) if object_type: object_type_field = object_type._meta.fields.get(field_name) if object_type_field: field_type = object_type_field.type if isinstance(field_type, graphene.NonNull): field_type = field_type.of_type return field_type return None def get_filtering_args_from_filterset(filterset_class, type): """ Inspect a FilterSet and produce the arguments to pass to a Graphene Field. These arguments will be available to filter against in the GraphQL API. """ from ..forms.converter import convert_form_field args = {} model = filterset_class._meta.model registry = type._meta.registry for name, filter_field in filterset_class.base_filters.items(): filter_type = filter_field.lookup_expr required = filter_field.extra.get("required", False) field_type = None form_field = None if ( isinstance(filter_field, TypedFilter) and filter_field.input_type is not None ): # First check if the filter input type has been explicitly given field_type = filter_field.input_type else: if name not in filterset_class.declared_filters or isinstance( filter_field, TypedFilter ): # Get the filter field for filters that are no explicitly declared. if filter_type == "isnull": field_type = graphene.Boolean else: model_field = get_model_field(model, filter_field.field_name) # Get the form field either from: # 1. the formfield corresponding to the model field # 2. the field defined on filter if hasattr(model_field, "formfield"): form_field = model_field.formfield(required=required) if not form_field: form_field = filter_field.field # First try to get the matching field type from the GraphQL DjangoObjectType if model_field: if ( isinstance(form_field, forms.ModelChoiceField) or isinstance(form_field, forms.ModelMultipleChoiceField) or isinstance(form_field, GlobalIDMultipleChoiceField) or isinstance(form_field, GlobalIDFormField) ): # Foreign key have dynamic types and filtering on a foreign key actually means filtering on its ID. field_type = get_field_type( registry, model_field.related_model, "id" ) else: field_type = get_field_type( registry, model_field.model, model_field.name ) if not field_type: # Fallback on converting the form field either because: # - it's an explicitly declared filters # - we did not manage to get the type from the model type form_field = form_field or filter_field.field field_type = convert_form_field(form_field).get_type() if isinstance(filter_field, ListFilter) or isinstance( filter_field, RangeFilter ): # Replace InFilter/RangeFilter filters (`in`, `range`) argument type to be a list of # the same type as the field. See comments in `replace_csv_filters` method for more details. field_type = graphene.List(field_type) args[name] = graphene.Argument( field_type, description=filter_field.label, required=required, ) return args def get_filterset_class(filterset_class, **meta): """ Get the class to be used as the FilterSet. """ if filterset_class: # If were given a FilterSet class, then set it up. graphene_filterset_class = setup_filterset(filterset_class) else: # Otherwise create one. graphene_filterset_class = custom_filterset_factory(**meta) replace_csv_filters(graphene_filterset_class) return graphene_filterset_class def replace_csv_filters(filterset_class): """ Replace the "in" and "range" filters (that are not explicitly declared) to not be BaseCSVFilter (BaseInFilter, BaseRangeFilter) objects anymore but our custom InFilter/RangeFilter filter class that use the input value as filter argument on the queryset. This is because those BaseCSVFilter are expecting a string as input with comma separated values. But with GraphQl we can actually have a list as input and have a proper type verification of each value in the list. See issue https://github.com/graphql-python/graphene-django/issues/1068. """ for name, filter_field in list(filterset_class.base_filters.items()): # Do not touch any declared filters if name in filterset_class.declared_filters: continue filter_type = filter_field.lookup_expr if filter_type == "in": filterset_class.base_filters[name] = ListFilter( field_name=filter_field.field_name, lookup_expr=filter_field.lookup_expr, label=filter_field.label, method=filter_field.method, exclude=filter_field.exclude, **filter_field.extra, ) elif filter_type == "range": filterset_class.base_filters[name] = RangeFilter( field_name=filter_field.field_name, lookup_expr=filter_field.lookup_expr, label=filter_field.label, method=filter_field.method, exclude=filter_field.exclude, **filter_field.extra, ) django-graphene-3.2.3/graphene_django/forms/000077500000000000000000000000001476451357100210265ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/forms/__init__.py000066400000000000000000000001121476451357100231310ustar00rootroot00000000000000from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField # noqa django-graphene-3.2.3/graphene_django/forms/converter.py000066400000000000000000000066011476451357100234120ustar00rootroot00000000000000from functools import singledispatch from django import forms from django.core.exceptions import ImproperlyConfigured from graphene import ( ID, UUID, Boolean, Date, DateTime, Decimal, Float, Int, List, String, Time, ) from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField def get_form_field_description(field): return str(field.help_text) if field.help_text else None @singledispatch def convert_form_field(field): raise ImproperlyConfigured( f"Don't know how to convert the Django form field {field} ({field.__class__}) " "to Graphene type" ) @convert_form_field.register(forms.fields.BaseTemporalField) @convert_form_field.register(forms.CharField) @convert_form_field.register(forms.EmailField) @convert_form_field.register(forms.SlugField) @convert_form_field.register(forms.URLField) @convert_form_field.register(forms.ChoiceField) @convert_form_field.register(forms.RegexField) @convert_form_field.register(forms.Field) def convert_form_field_to_string(field): return String( description=get_form_field_description(field), required=field.required ) @convert_form_field.register(forms.UUIDField) def convert_form_field_to_uuid(field): return UUID(description=get_form_field_description(field), required=field.required) @convert_form_field.register(forms.IntegerField) @convert_form_field.register(forms.NumberInput) def convert_form_field_to_int(field): return Int(description=get_form_field_description(field), required=field.required) @convert_form_field.register(forms.BooleanField) def convert_form_field_to_boolean(field): return Boolean( description=get_form_field_description(field), required=field.required ) @convert_form_field.register(forms.NullBooleanField) def convert_form_field_to_nullboolean(field): return Boolean(description=get_form_field_description(field)) @convert_form_field.register(forms.FloatField) def convert_form_field_to_float(field): return Float(description=get_form_field_description(field), required=field.required) @convert_form_field.register(forms.DecimalField) def convert_form_field_to_decimal(field): return Decimal( description=get_form_field_description(field), required=field.required ) @convert_form_field.register(forms.MultipleChoiceField) def convert_form_field_to_string_list(field): return List( String, description=get_form_field_description(field), required=field.required ) @convert_form_field.register(forms.ModelMultipleChoiceField) @convert_form_field.register(GlobalIDMultipleChoiceField) def convert_form_field_to_id_list(field): return List(ID, required=field.required) @convert_form_field.register(forms.DateField) def convert_form_field_to_date(field): return Date(description=get_form_field_description(field), required=field.required) @convert_form_field.register(forms.DateTimeField) def convert_form_field_to_datetime(field): return DateTime( description=get_form_field_description(field), required=field.required ) @convert_form_field.register(forms.TimeField) def convert_form_field_to_time(field): return Time(description=get_form_field_description(field), required=field.required) @convert_form_field.register(forms.ModelChoiceField) @convert_form_field.register(GlobalIDFormField) def convert_form_field_to_id(field): return ID(required=field.required) django-graphene-3.2.3/graphene_django/forms/forms.py000066400000000000000000000023171476451357100225310ustar00rootroot00000000000000import binascii from django.core.exceptions import ValidationError from django.forms import CharField, Field, MultipleChoiceField from django.utils.translation import gettext_lazy as _ from graphql_relay import from_global_id class GlobalIDFormField(Field): default_error_messages = {"invalid": _("Invalid ID specified.")} def clean(self, value): if not value and not self.required: return None try: _type, _id = from_global_id(value) except (TypeError, ValueError, UnicodeDecodeError, binascii.Error): raise ValidationError(self.error_messages["invalid"]) try: CharField().clean(_id) CharField().clean(_type) except ValidationError: raise ValidationError(self.error_messages["invalid"]) return value class GlobalIDMultipleChoiceField(MultipleChoiceField): default_error_messages = { "invalid_choice": _("One of the specified IDs was invalid (%(value)s)."), "invalid_list": _("Enter a list of values."), } def valid_value(self, value): # Clean will raise a validation error if there is a problem GlobalIDFormField().clean(value) return True django-graphene-3.2.3/graphene_django/forms/mutation.py000066400000000000000000000133611476451357100232440ustar00rootroot00000000000000# from django import forms from collections import OrderedDict import graphene from graphene import Field, InputField from graphene.relay.mutation import ClientIDMutation from graphene.types.mutation import MutationOptions # from graphene.types.inputobjecttype import ( # InputObjectTypeOptions, # InputObjectType, # ) from graphene.types.utils import yank_fields_from_attrs from graphene_django.constants import MUTATION_ERRORS_FLAG from graphene_django.registry import get_global_registry from ..types import ErrorType from .converter import convert_form_field def fields_for_form(form, only_fields, exclude_fields): fields = OrderedDict() for name, field in form.fields.items(): is_not_in_only = only_fields and name not in only_fields is_excluded = ( name in exclude_fields # or # name in already_created_fields ) if is_not_in_only or is_excluded: continue fields[name] = convert_form_field(field) return fields class BaseDjangoFormMutation(ClientIDMutation): class Meta: abstract = True @classmethod def mutate_and_get_payload(cls, root, info, **input): form = cls.get_form(root, info, **input) if form.is_valid(): return cls.perform_mutate(form, info) else: errors = ErrorType.from_errors(form.errors) _set_errors_flag_to_context(info) return cls(errors=errors, **form.data) @classmethod def get_form(cls, root, info, **input): form_kwargs = cls.get_form_kwargs(root, info, **input) return cls._meta.form_class(**form_kwargs) @classmethod def get_form_kwargs(cls, root, info, **input): kwargs = {"data": input} pk = input.pop("id", None) if pk: instance = cls._meta.model._default_manager.get(pk=pk) kwargs["instance"] = instance return kwargs class DjangoFormMutationOptions(MutationOptions): form_class = None class DjangoFormMutation(BaseDjangoFormMutation): class Meta: abstract = True errors = graphene.List(ErrorType) @classmethod def __init_subclass_with_meta__( cls, form_class=None, only_fields=(), exclude_fields=(), **options ): if not form_class: raise Exception("form_class is required for DjangoFormMutation") form = form_class() input_fields = fields_for_form(form, only_fields, exclude_fields) output_fields = fields_for_form(form, only_fields, exclude_fields) _meta = DjangoFormMutationOptions(cls) _meta.form_class = form_class _meta.fields = yank_fields_from_attrs(output_fields, _as=Field) input_fields = yank_fields_from_attrs(input_fields, _as=InputField) super().__init_subclass_with_meta__( _meta=_meta, input_fields=input_fields, **options ) @classmethod def perform_mutate(cls, form, info): if hasattr(form, "save"): # `save` method won't exist on plain Django forms, but this mutation can # in theory be used with `ModelForm`s as well and we do want to save them. form.save() return cls(errors=[], **form.cleaned_data) class DjangoModelDjangoFormMutationOptions(DjangoFormMutationOptions): model = None return_field_name = None class DjangoModelFormMutation(BaseDjangoFormMutation): class Meta: abstract = True errors = graphene.List(graphene.NonNull(ErrorType), required=True) @classmethod def __init_subclass_with_meta__( cls, form_class=None, model=None, return_field_name=None, only_fields=(), exclude_fields=(), **options, ): if not form_class: raise Exception("form_class is required for DjangoModelFormMutation") if not model: model = form_class._meta.model if not model: raise Exception("model is required for DjangoModelFormMutation") form = form_class() input_fields = fields_for_form(form, only_fields, exclude_fields) if "id" not in exclude_fields: input_fields["id"] = graphene.ID() registry = get_global_registry() model_type = registry.get_type_for_model(model) if not model_type: raise Exception(f"No type registered for model: {model.__name__}") if not return_field_name: model_name = model.__name__ return_field_name = model_name[:1].lower() + model_name[1:] output_fields = OrderedDict() output_fields[return_field_name] = graphene.Field(model_type) _meta = DjangoModelDjangoFormMutationOptions(cls) _meta.form_class = form_class _meta.model = model _meta.return_field_name = return_field_name _meta.fields = yank_fields_from_attrs(output_fields, _as=Field) input_fields = yank_fields_from_attrs(input_fields, _as=InputField) super().__init_subclass_with_meta__( _meta=_meta, input_fields=input_fields, **options ) @classmethod def mutate_and_get_payload(cls, root, info, **input): form = cls.get_form(root, info, **input) if form.is_valid(): return cls.perform_mutate(form, info) else: errors = ErrorType.from_errors(form.errors) _set_errors_flag_to_context(info) return cls(errors=errors) @classmethod def perform_mutate(cls, form, info): obj = form.save() kwargs = {cls._meta.return_field_name: obj} return cls(errors=[], **kwargs) def _set_errors_flag_to_context(info): # This is not ideal but necessary to keep the response errors empty if info and info.context: setattr(info.context, MUTATION_ERRORS_FLAG, True) django-graphene-3.2.3/graphene_django/forms/tests/000077500000000000000000000000001476451357100221705ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/forms/tests/__init__.py000066400000000000000000000000001476451357100242670ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/forms/tests/test_converter.py000066400000000000000000000063261476451357100256170ustar00rootroot00000000000000from django import VERSION as DJANGO_VERSION, forms from pytest import raises from graphene import ( ID, UUID, Boolean, Date, DateTime, Decimal, Float, Int, List, NonNull, String, Time, ) from ..converter import convert_form_field def assert_conversion(django_field, graphene_field, *args, **kwargs): # Arrange help_text = kwargs.setdefault("help_text", "Custom Help Text") field = django_field(*args, **kwargs) # Act graphene_type = convert_form_field(field) # Assert assert isinstance(graphene_type, graphene_field) field = graphene_type.Field() assert field.description == help_text return field def test_should_unknown_django_field_raise_exception(): with raises(Exception) as excinfo: convert_form_field(None) assert "Don't know how to convert the Django form field" in str(excinfo.value) def test_should_date_convert_date(): assert_conversion(forms.DateField, Date) def test_should_time_convert_time(): assert_conversion(forms.TimeField, Time) def test_should_date_time_convert_date_time(): assert_conversion(forms.DateTimeField, DateTime) def test_should_char_convert_string(): assert_conversion(forms.CharField, String) def test_should_email_convert_string(): assert_conversion(forms.EmailField, String) def test_should_slug_convert_string(): assert_conversion(forms.SlugField, String) def test_should_url_convert_string(): kwargs = {} if DJANGO_VERSION >= (5, 0): # silence RemovedInDjango60Warning kwargs["assume_scheme"] = "https" assert_conversion(forms.URLField, String, **kwargs) def test_should_choice_convert_string(): assert_conversion(forms.ChoiceField, String) def test_should_base_field_convert_string(): assert_conversion(forms.Field, String) def test_should_regex_convert_string(): assert_conversion(forms.RegexField, String, "[0-9]+") def test_should_uuid_convert_string(): assert_conversion(forms.UUIDField, UUID) def test_should_integer_convert_int(): assert_conversion(forms.IntegerField, Int) def test_should_boolean_convert_boolean(): field = assert_conversion(forms.BooleanField, Boolean) assert isinstance(field.type, NonNull) def test_should_nullboolean_convert_boolean(): field = assert_conversion(forms.NullBooleanField, Boolean) assert not isinstance(field.type, NonNull) def test_should_float_convert_float(): assert_conversion(forms.FloatField, Float) def test_should_decimal_convert_decimal(): assert_conversion(forms.DecimalField, Decimal) def test_should_multiple_choice_convert_list(): field = forms.MultipleChoiceField() graphene_type = convert_form_field(field) assert isinstance(graphene_type, List) assert graphene_type.of_type == String def test_should_model_multiple_choice_convert_connectionorlist(): field = forms.ModelMultipleChoiceField(queryset=None) graphene_type = convert_form_field(field) assert isinstance(graphene_type, List) assert graphene_type.of_type == ID def test_should_manytoone_convert_connectionorlist(): field = forms.ModelChoiceField(queryset=None) graphene_type = convert_form_field(field) assert isinstance(graphene_type, ID) django-graphene-3.2.3/graphene_django/forms/tests/test_djangoinputobject.py000066400000000000000000000243111476451357100273130ustar00rootroot00000000000000from django import forms from pytest import raises import graphene from graphene_django import DjangoObjectType from ...tests.models import CHOICES, Film, Reporter from ..types import DjangoFormInputObjectType # Reporter a_choice CHOICES = ((1, "this"), (2, _("that"))) THIS = CHOICES[0][0] THIS_ON_CLIENT_CONVERTED = "A_1" # Film genre choices=[("do", "Documentary"), ("ac", "Action"), ("ot", "Other")], DOCUMENTARY = "do" DOCUMENTARY_ON_CLIENT_CONVERTED = "DO" class FilmForm(forms.ModelForm): class Meta: model = Film exclude = () class ReporterType(DjangoObjectType): class Meta: model = Reporter fields = "__all__" class ReporterForm(forms.ModelForm): class Meta: model = Reporter exclude = ("pets", "email", "fans") class MyForm(forms.Form): text_field = forms.CharField() int_field = forms.IntegerField() def test_needs_form_class(): with raises(Exception) as exc: class MyInputType(DjangoFormInputObjectType): pass assert exc.value.args[0] == "form_class is required for DjangoFormInputObjectType" def test_type_from_modelform_has_input_fields(): class ReporterInputType(DjangoFormInputObjectType): class Meta: form_class = ReporterForm only_fields = ("first_name", "last_name", "a_choice") fields = ["first_name", "last_name", "a_choice", "id"] assert all(f in ReporterInputType._meta.fields for f in fields) def test_type_from_form_has_input_fields(): class MyFormInputType(DjangoFormInputObjectType): class Meta: form_class = MyForm fields = ["text_field", "int_field", "id"] assert all(f in MyFormInputType._meta.fields for f in fields) def test_type_custom_id_field(): class MyFormInputType(DjangoFormInputObjectType): class Meta: form_class = MyForm add_id_field_name = "pk" fields = ["text_field", "int_field", "pk"] assert all(f in MyFormInputType._meta.fields for f in fields) assert MyFormInputType._meta.fields["pk"].type is graphene.ID def test_type_custom_id_field_type(): class MyFormInputType(DjangoFormInputObjectType): class Meta: form_class = MyForm add_id_field_name = "pk" add_id_field_type = graphene.String(required=False) fields = ["text_field", "int_field", "pk"] assert all(f in MyFormInputType._meta.fields for f in fields) assert MyFormInputType._meta.fields["pk"].type is graphene.String class MockQuery(graphene.ObjectType): a = graphene.String() def test_mutation_with_form_djangoforminputtype(): class MyFormInputType(DjangoFormInputObjectType): class Meta: form_class = MyForm class MyFormMutation(graphene.Mutation): class Arguments: form_data = MyFormInputType(required=True) result = graphene.Boolean() def mutate(_root, _info, form_data): form = MyForm(data=form_data) if form.is_valid(): result = form.cleaned_data == { "text_field": "text", "int_field": 777, } return MyFormMutation(result=result) return MyFormMutation(result=False) class Mutation(graphene.ObjectType): myForm_mutation = MyFormMutation.Field() schema = graphene.Schema(query=MockQuery, mutation=Mutation) result = schema.execute( """ mutation MyFormMutation($formData: MyFormInputType!) { myFormMutation(formData: $formData) { result } } """, variable_values={"formData": {"textField": "text", "intField": 777}}, ) assert result.errors is None assert result.data == {"myFormMutation": {"result": True}} def test_mutation_with_modelform_djangoforminputtype(): class ReporterInputType(DjangoFormInputObjectType): class Meta: form_class = ReporterForm object_type = ReporterType only_fields = ("first_name", "last_name", "a_choice") class ReporterMutation(graphene.Mutation): class Arguments: reporter_data = ReporterInputType(required=True) result = graphene.Field(ReporterType) def mutate(_root, _info, reporter_data): reporter = Reporter.objects.get(pk=reporter_data.id) form = ReporterForm(data=reporter_data, instance=reporter) if form.is_valid(): reporter = form.save() return ReporterMutation(result=reporter) return ReporterMutation(result=None) class Mutation(graphene.ObjectType): report_mutation = ReporterMutation.Field() schema = graphene.Schema(query=MockQuery, mutation=Mutation) reporter = Reporter.objects.create( first_name="Bob", last_name="Roberts", a_choice=THIS ) result = schema.execute( """ mutation ReportMutation($reporterData: ReporterInputType!) { reportMutation(reporterData: $reporterData) { result { id, firstName, lastName, aChoice } } } """, variable_values={ "reporterData": { "id": reporter.pk, "firstName": "Dave", "lastName": "Smith", "aChoice": THIS_ON_CLIENT_CONVERTED, } }, ) assert result.errors is None assert result.data["reportMutation"]["result"] == { "id": "1", "firstName": "Dave", "lastName": "Smith", "aChoice": THIS_ON_CLIENT_CONVERTED, } assert Reporter.objects.count() == 1 reporter.refresh_from_db() assert reporter.first_name == "Dave" def reporter_enum_convert_mutation_result( ReporterInputType, choice_val_on_client=THIS_ON_CLIENT_CONVERTED ): class ReporterMutation(graphene.Mutation): class Arguments: reporter = ReporterInputType(required=True) result_str = graphene.String() result_int = graphene.Int() def mutate(_root, _info, reporter): if isinstance(reporter.a_choice, int) or reporter.a_choice.isdigit(): return ReporterMutation(result_int=reporter.a_choice, result_str=None) return ReporterMutation(result_int=None, result_str=reporter.a_choice) class Mutation(graphene.ObjectType): report_mutation = ReporterMutation.Field() schema = graphene.Schema(query=MockQuery, mutation=Mutation) return schema.execute( """ mutation ReportMutation($reporter: ReporterInputType!) { reportMutation(reporter: $reporter) { resultStr, resultInt } } """, variable_values={"reporter": {"aChoice": choice_val_on_client}}, ) def test_enum_not_converted(): class ReporterInputType(DjangoFormInputObjectType): class Meta: form_class = ReporterForm only_fields = ("a_choice",) result = reporter_enum_convert_mutation_result(ReporterInputType) assert result.errors is None assert result.data["reportMutation"]["resultStr"] == THIS_ON_CLIENT_CONVERTED assert result.data["reportMutation"]["resultInt"] is None assert ReporterInputType._meta.fields["a_choice"].type is graphene.String def test_enum_is_converted_to_original(): class ReporterInputType(DjangoFormInputObjectType): class Meta: form_class = ReporterForm object_type = ReporterType only_fields = ("a_choice",) result = reporter_enum_convert_mutation_result(ReporterInputType) assert result.errors is None assert result.data["reportMutation"]["resultInt"] == THIS assert result.data["reportMutation"]["resultStr"] is None assert ( ReporterInputType._meta.fields["a_choice"].type.__name__ == "AChoiceEnumBackConvString" ) def test_convert_choices_to_enum_is_false_and_field_type_as_in_model(): class ReporterTypeNotConvertChoices(DjangoObjectType): class Meta: model = Reporter convert_choices_to_enum = False fields = "__all__" class ReporterInputType(DjangoFormInputObjectType): class Meta: form_class = ReporterForm object_type = ReporterTypeNotConvertChoices only_fields = ("a_choice",) result = reporter_enum_convert_mutation_result(ReporterInputType, THIS) assert result.errors is None assert result.data["reportMutation"]["resultInt"] == THIS assert result.data["reportMutation"]["resultStr"] is None assert ReporterInputType._meta.fields["a_choice"].type is graphene.Int def enum_convert_mutation_result_film(FilmInputType): class FilmMutation(graphene.Mutation): class Arguments: film = FilmInputType(required=True) result = graphene.String() def mutate(_root, _info, film): return FilmMutation(result=film.genre) class Mutation(graphene.ObjectType): film_mutation = FilmMutation.Field() schema = graphene.Schema(query=MockQuery, mutation=Mutation) return schema.execute( """ mutation FilmMutation($film: FilmInputType!) { filmMutation(film: $film) { result } } """, variable_values={"film": {"genre": DOCUMENTARY_ON_CLIENT_CONVERTED}}, ) def test_enum_not_converted_required_non_number(): class FilmInputType(DjangoFormInputObjectType): class Meta: form_class = FilmForm only_fields = ("genre",) result = enum_convert_mutation_result_film(FilmInputType) assert result.errors is None assert result.data["filmMutation"]["result"] == DOCUMENTARY_ON_CLIENT_CONVERTED def test_enum_is_converted_to_original_required_non_number(): class FilmType(DjangoObjectType): class Meta: model = Film fields = "__all__" class FilmInputType(DjangoFormInputObjectType): class Meta: form_class = FilmForm object_type = FilmType only_fields = ("genre",) result = enum_convert_mutation_result_film(FilmInputType) assert result.errors is None assert result.data["filmMutation"]["result"] == DOCUMENTARY django-graphene-3.2.3/graphene_django/forms/tests/test_mutation.py000066400000000000000000000237761476451357100254600ustar00rootroot00000000000000from django import forms from django.core.exceptions import ValidationError from pytest import raises from graphene import Field, ObjectType, Schema, String from graphene_django import DjangoObjectType from graphene_django.tests.forms import PetForm from graphene_django.tests.models import Pet from graphene_django.tests.mutations import PetMutation from ..mutation import DjangoFormMutation, DjangoModelFormMutation class MyForm(forms.Form): text = forms.CharField() def clean_text(self): text = self.cleaned_data["text"] if text == "INVALID_INPUT": raise ValidationError("Invalid input") return text def save(self): pass def test_needs_form_class(): with raises(Exception) as exc: class MyMutation(DjangoFormMutation): pass assert exc.value.args[0] == "form_class is required for DjangoFormMutation" def test_has_output_fields(): class MyMutation(DjangoFormMutation): class Meta: form_class = MyForm assert "errors" in MyMutation._meta.fields def test_has_input_fields(): class MyMutation(DjangoFormMutation): class Meta: form_class = MyForm assert "text" in MyMutation.Input._meta.fields def test_mutation_error_camelcased(graphene_settings): class ExtraPetForm(PetForm): test_field = forms.CharField(required=True) class PetType(DjangoObjectType): class Meta: model = Pet fields = "__all__" class PetMutation(DjangoModelFormMutation): pet = Field(PetType) class Meta: form_class = ExtraPetForm result = PetMutation.mutate_and_get_payload(None, None) assert {f.field for f in result.errors} == {"name", "age", "testField"} graphene_settings.CAMELCASE_ERRORS = False result = PetMutation.mutate_and_get_payload(None, None) assert {f.field for f in result.errors} == {"name", "age", "test_field"} class MockQuery(ObjectType): a = String() def test_form_invalid_form(): class MyMutation(DjangoFormMutation): class Meta: form_class = MyForm class Mutation(ObjectType): my_mutation = MyMutation.Field() schema = Schema(query=MockQuery, mutation=Mutation) result = schema.execute( """ mutation MyMutation { myMutation(input: { text: "INVALID_INPUT" }) { errors { field messages } text } } """ ) assert result.errors is None assert result.data["myMutation"]["errors"] == [ {"field": "text", "messages": ["Invalid input"]} ] def test_form_valid_input(): class MyMutation(DjangoFormMutation): class Meta: form_class = MyForm class Mutation(ObjectType): my_mutation = MyMutation.Field() schema = Schema(query=MockQuery, mutation=Mutation) result = schema.execute( """ mutation MyMutation { myMutation(input: { text: "VALID_INPUT" }) { errors { field messages } text } } """ ) assert result.errors is None assert result.data["myMutation"]["errors"] == [] assert result.data["myMutation"]["text"] == "VALID_INPUT" def test_default_meta_fields(): assert PetMutation._meta.model is Pet assert PetMutation._meta.return_field_name == "pet" assert "pet" in PetMutation._meta.fields def test_default_input_meta_fields(): assert PetMutation._meta.model is Pet assert PetMutation._meta.return_field_name == "pet" assert "name" in PetMutation.Input._meta.fields assert "client_mutation_id" in PetMutation.Input._meta.fields assert "id" in PetMutation.Input._meta.fields def test_exclude_fields_input_meta_fields(): class PetType(DjangoObjectType): class Meta: model = Pet fields = "__all__" class PetMutation(DjangoModelFormMutation): pet = Field(PetType) class Meta: form_class = PetForm exclude_fields = ["id"] assert PetMutation._meta.model is Pet assert PetMutation._meta.return_field_name == "pet" assert "name" in PetMutation.Input._meta.fields assert "age" in PetMutation.Input._meta.fields assert "client_mutation_id" in PetMutation.Input._meta.fields assert "id" not in PetMutation.Input._meta.fields def test_custom_return_field_name(): class PetType(DjangoObjectType): class Meta: model = Pet fields = "__all__" class PetMutation(DjangoModelFormMutation): pet = Field(PetType) class Meta: form_class = PetForm model = Pet return_field_name = "animal" assert PetMutation._meta.model is Pet assert PetMutation._meta.return_field_name == "animal" assert "animal" in PetMutation._meta.fields def test_model_form_mutation_mutate_existing(): class Mutation(ObjectType): pet_mutation = PetMutation.Field() schema = Schema(query=MockQuery, mutation=Mutation) pet = Pet.objects.create(name="Axel", age=10) result = schema.execute( """ mutation PetMutation($pk: ID!) { petMutation(input: { id: $pk, name: "Mia", age: 10 }) { pet { name age } } } """, variable_values={"pk": pet.pk}, ) assert result.errors is None assert result.data["petMutation"]["pet"] == {"name": "Mia", "age": 10} assert Pet.objects.count() == 1 pet.refresh_from_db() assert pet.name == "Mia" def test_model_form_mutation_creates_new(): class Mutation(ObjectType): pet_mutation = PetMutation.Field() schema = Schema(query=MockQuery, mutation=Mutation) result = schema.execute( """ mutation PetMutation { petMutation(input: { name: "Mia", age: 10 }) { pet { name age } errors { field messages } } } """ ) assert result.errors is None assert result.data["petMutation"]["pet"] == {"name": "Mia", "age": 10} assert Pet.objects.count() == 1 pet = Pet.objects.get() assert pet.name == "Mia" assert pet.age == 10 def test_model_form_mutation_invalid_input(): class Mutation(ObjectType): pet_mutation = PetMutation.Field() schema = Schema(query=MockQuery, mutation=Mutation) result = schema.execute( """ mutation PetMutation { petMutation(input: { name: "Mia", age: 99 }) { pet { name age } errors { field messages } } } """ ) assert result.errors is None assert result.data["petMutation"]["pet"] is None assert result.data["petMutation"]["errors"] == [ {"field": "age", "messages": ["Too old"]} ] assert Pet.objects.count() == 0 def test_model_form_mutation_mutate_invalid_form(): result = PetMutation.mutate_and_get_payload(None, None) # A pet was not created assert Pet.objects.count() == 0 fields_w_error = [e.field for e in result.errors] assert len(result.errors) == 2 assert result.errors[0].messages == ["This field is required."] assert result.errors[1].messages == ["This field is required."] assert "age" in fields_w_error assert "name" in fields_w_error def test_model_form_mutation_multiple_creation_valid(): class Mutation(ObjectType): pet_mutation = PetMutation.Field() schema = Schema(query=MockQuery, mutation=Mutation) result = schema.execute( """ mutation PetMutations { petMutation1: petMutation(input: { name: "Mia", age: 10 }) { pet { name age } errors { field messages } } petMutation2: petMutation(input: { name: "Enzo", age: 0 }) { pet { name age } errors { field messages } } } """ ) assert result.errors is None assert result.data["petMutation1"]["pet"] == {"name": "Mia", "age": 10} assert result.data["petMutation2"]["pet"] == {"name": "Enzo", "age": 0} assert Pet.objects.count() == 2 pet1 = Pet.objects.first() assert pet1.name == "Mia" assert pet1.age == 10 pet2 = Pet.objects.last() assert pet2.name == "Enzo" assert pet2.age == 0 def test_model_form_mutation_multiple_creation_invalid(): class Mutation(ObjectType): pet_mutation = PetMutation.Field() schema = Schema(query=MockQuery, mutation=Mutation) result = schema.execute( """ mutation PetMutations { petMutation1: petMutation(input: { name: "Mia", age: 99 }) { pet { name age } errors { field messages } } petMutation2: petMutation(input: { name: "Enzo", age: 0 }) { pet { name age } errors { field messages } } } """ ) assert result.errors is None assert result.data["petMutation1"]["pet"] is None assert result.data["petMutation1"]["errors"] == [ {"field": "age", "messages": ["Too old"]} ] assert result.data["petMutation2"]["pet"] == {"name": "Enzo", "age": 0} assert Pet.objects.count() == 1 pet = Pet.objects.get() assert pet.name == "Enzo" assert pet.age == 0 django-graphene-3.2.3/graphene_django/forms/types.py000066400000000000000000000111001476451357100225350ustar00rootroot00000000000000import graphene from graphene import ID from graphene.types.inputobjecttype import InputObjectType from graphene.utils.str_converters import to_camel_case from ..converter import EnumValueField from ..types import ErrorType # noqa Import ErrorType for backwards compatibility from .mutation import fields_for_form class DjangoFormInputObjectType(InputObjectType): @classmethod def __init_subclass_with_meta__( cls, container=None, _meta=None, only_fields=(), exclude_fields=(), form_class=None, object_type=None, add_id_field_name=None, add_id_field_type=None, **options, ): """Retrieve fields from django form (Meta.form_class). Received fields are set to cls (they will be converted to input fields by InputObjectType). Type of fields with choices (converted to enum) is set to custom scalar type (using Meta.object_type) to dynamically convert enum values back. class MyDjangoFormInput(DjangoFormInputObjectType): # any other fields can be placed here and other inputobjectforms as well class Meta: form_class = MyDjangoModelForm object_type = MyModelType class SomeMutation(graphene.Mutation): class Arguments: data = MyDjangoFormInput(required=True) @staticmethod def mutate(_root, _info, data): form_inst = MyDjangoModelForm(data=data) if form_inst.is_valid(): django_model_instance = form_inst.save(commit=False) # ... etc ... """ if not form_class: raise Exception("form_class is required for DjangoFormInputObjectType") form = form_class() form_fields = fields_for_form(form, only_fields, exclude_fields) for name, field in form_fields.items(): if ( object_type and name in object_type._meta.fields and isinstance(object_type._meta.fields[name], EnumValueField) ): # Field type EnumValueField here means that field # with choices have been converted to enum setattr(cls, name, cls.get_enum_cnv_cls_instance(name, object_type)) elif ( object_type and name in object_type._meta.fields and object_type._meta.convert_choices_to_enum is False and form.fields[name].__class__.__name__ == "TypedChoiceField" ): # FIXME # in case if convert_choices_to_enum is False # form field class is converted to String but original # model field type is needed here... (.converter.py bug?) # This is temp workaround to get field type from ObjectType field # TEST: test_enum_not_converted_and_field_type_as_in_model setattr(cls, name, object_type._meta.fields[name].type()) else: # set input field according to django form field setattr(cls, name, field) # explicitly adding id field (absent in django form fields) # with name and type from Meta or 'id' with graphene.ID by default if add_id_field_name: setattr(cls, add_id_field_name, add_id_field_type or ID(required=False)) elif "id" not in exclude_fields: cls.id = ID(required=False) super().__init_subclass_with_meta__(container=container, _meta=_meta, **options) @staticmethod def get_enum_cnv_cls_instance(field_name, object_type): """Saves args in context to convert enum values in Dynamically created Scalar derived class """ @staticmethod def parse_value(value): # field_name & object_type have been saved in context (closure) field = object_type._meta.fields[field_name] if isinstance(field.type, graphene.NonNull): val_before_convert = field.type._of_type[value].value else: val_before_convert = field.type[value].value return graphene.String.parse_value(val_before_convert) cls_doc = "String scalar to convert choice value back from enum to original" scalar_type = type( ( f"{field_name[0].upper()}{to_camel_case(field_name[1:])}" "EnumBackConvString" ), (graphene.String,), {"parse_value": parse_value, "__doc__": cls_doc}, ) return scalar_type() django-graphene-3.2.3/graphene_django/management/000077500000000000000000000000001476451357100220145ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/management/__init__.py000066400000000000000000000000001476451357100241130ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/management/commands/000077500000000000000000000000001476451357100236155ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/management/commands/__init__.py000066400000000000000000000000001476451357100257140ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/management/commands/graphql_schema.py000066400000000000000000000070701476451357100271510ustar00rootroot00000000000000import functools import importlib import json import os from django.core.management.base import BaseCommand, CommandError from django.utils import autoreload from graphql import print_schema from graphene_django.settings import graphene_settings class CommandArguments(BaseCommand): def add_arguments(self, parser): parser.add_argument( "--schema", type=str, dest="schema", default=graphene_settings.SCHEMA, help="Django app containing schema to dump, e.g. myproject.core.schema.schema", ) parser.add_argument( "--out", type=str, dest="out", default=graphene_settings.SCHEMA_OUTPUT, help="Output file, --out=- prints to stdout (default: schema.json)", ) parser.add_argument( "--indent", type=int, dest="indent", default=graphene_settings.SCHEMA_INDENT, help="Output file indent (default: None)", ) parser.add_argument( "--watch", dest="watch", default=False, action="store_true", help="Updates the schema on file changes (default: False)", ) class Command(CommandArguments): help = "Dump Graphene schema as a JSON or GraphQL file" can_import_settings = True requires_system_checks = [] def save_json_file(self, out, schema_dict, indent): with open(out, "w") as outfile: json.dump(schema_dict, outfile, indent=indent, sort_keys=True) def save_graphql_file(self, out, schema): with open(out, "w", encoding="utf-8") as outfile: outfile.write(print_schema(schema.graphql_schema)) def get_schema(self, schema, out, indent): schema_dict = {"data": schema.introspect()} if out == "-" or out == "-.json": self.stdout.write(json.dumps(schema_dict, indent=indent, sort_keys=True)) elif out == "-.graphql": self.stdout.write(print_schema(schema.graphql_schema)) else: # Determine format _, file_extension = os.path.splitext(out) if file_extension == ".graphql": self.save_graphql_file(out, schema) elif file_extension == ".json": self.save_json_file(out, schema_dict, indent) else: raise CommandError(f'Unrecognised file format "{file_extension}"') style = getattr(self, "style", None) success = getattr(style, "SUCCESS", lambda x: x) self.stdout.write(success(f"Successfully dumped GraphQL schema to {out}")) def handle(self, *args, **options): options_schema = options.get("schema") if options_schema and isinstance(options_schema, str): module_str, schema_name = options_schema.rsplit(".", 1) mod = importlib.import_module(module_str) schema = getattr(mod, schema_name) elif options_schema: schema = options_schema else: schema = graphene_settings.SCHEMA out = options.get("out") or graphene_settings.SCHEMA_OUTPUT if not schema: raise CommandError( "Specify schema on GRAPHENE.SCHEMA setting or by using --schema" ) indent = options.get("indent") watch = options.get("watch") if watch: autoreload.run_with_reloader( functools.partial(self.get_schema, schema, out, indent) ) else: self.get_schema(schema, out, indent) django-graphene-3.2.3/graphene_django/registry.py000066400000000000000000000022161476451357100221230ustar00rootroot00000000000000class Registry: def __init__(self): self._registry = {} self._field_registry = {} def register(self, cls): from .types import DjangoObjectType assert issubclass( cls, DjangoObjectType ), f'Only DjangoObjectTypes can be registered, received "{cls.__name__}"' assert cls._meta.registry == self, "Registry for a Model have to match." # assert self.get_type_for_model(cls._meta.model) == cls, ( # 'Multiple DjangoObjectTypes registered for "{}"'.format(cls._meta.model) # ) if not getattr(cls._meta, "skip_registry", False): self._registry[cls._meta.model] = cls def get_type_for_model(self, model): return self._registry.get(model) def register_converted_field(self, field, converted): self._field_registry[field] = converted def get_converted_field(self, field): return self._field_registry.get(field) registry = None def get_global_registry(): global registry if not registry: registry = Registry() return registry def reset_global_registry(): global registry registry = None django-graphene-3.2.3/graphene_django/rest_framework/000077500000000000000000000000001476451357100227325ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/rest_framework/__init__.py000066400000000000000000000000001476451357100250310ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/rest_framework/models.py000066400000000000000000000012311476451357100245640ustar00rootroot00000000000000from django.db import models class MyFakeModel(models.Model): cool_name = models.CharField(max_length=50) created = models.DateTimeField(auto_now_add=True) class MyFakeModelWithPassword(models.Model): cool_name = models.CharField(max_length=50) password = models.CharField(max_length=50) class MyFakeModelWithDate(models.Model): cool_name = models.CharField(max_length=50) last_edited = models.DateField() class MyFakeModelWithChoiceField(models.Model): class ChoiceType(models.Choices): ASDF = "asdf" HI = "hi" choice_type = models.CharField( max_length=4, default=ChoiceType.HI.name, ) django-graphene-3.2.3/graphene_django/rest_framework/mutation.py000066400000000000000000000141061476451357100251460ustar00rootroot00000000000000from collections import OrderedDict from enum import Enum from django.shortcuts import get_object_or_404 from rest_framework import serializers import graphene from graphene.relay.mutation import ClientIDMutation from graphene.types import Field, InputField from graphene.types.mutation import MutationOptions from graphene.types.objecttype import yank_fields_from_attrs from ..types import ErrorType from .serializer_converter import convert_serializer_field class SerializerMutationOptions(MutationOptions): lookup_field = None model_class = None model_operations = ["create", "update"] serializer_class = None optional_fields = () def fields_for_serializer( serializer, only_fields, exclude_fields, is_input=False, convert_choices_to_enum=True, lookup_field=None, optional_fields=(), ): fields = OrderedDict() for name, field in serializer.fields.items(): is_not_in_only = only_fields and name not in only_fields is_excluded = any( [ name in exclude_fields, field.write_only and not is_input, # don't show write_only fields in Query field.read_only and is_input and lookup_field != name, # don't show read_only fields in Input isinstance( field, serializers.HiddenField ), # don't show hidden fields in Input ] ) if is_not_in_only or is_excluded: continue is_optional = name in optional_fields or "__all__" in optional_fields fields[name] = convert_serializer_field( field, is_input=is_input, convert_choices_to_enum=convert_choices_to_enum, force_optional=is_optional, ) return fields class SerializerMutation(ClientIDMutation): class Meta: abstract = True errors = graphene.List( ErrorType, description="May contain more than one error for same field." ) @classmethod def __init_subclass_with_meta__( cls, lookup_field=None, serializer_class=None, model_class=None, model_operations=("create", "update"), only_fields=(), exclude_fields=(), convert_choices_to_enum=True, _meta=None, optional_fields=(), **options, ): if not serializer_class: raise Exception("serializer_class is required for the SerializerMutation") if "update" not in model_operations and "create" not in model_operations: raise Exception('model_operations must contain "create" and/or "update"') serializer = serializer_class() if model_class is None: serializer_meta = getattr(serializer_class, "Meta", None) if serializer_meta: model_class = getattr(serializer_meta, "model", None) if lookup_field is None and model_class: lookup_field = model_class._meta.pk.name input_fields = fields_for_serializer( serializer, only_fields, exclude_fields, is_input=True, convert_choices_to_enum=convert_choices_to_enum, lookup_field=lookup_field, optional_fields=optional_fields, ) output_fields = fields_for_serializer( serializer, only_fields, exclude_fields, is_input=False, convert_choices_to_enum=convert_choices_to_enum, lookup_field=lookup_field, ) if not _meta: _meta = SerializerMutationOptions(cls) _meta.lookup_field = lookup_field _meta.model_operations = model_operations _meta.serializer_class = serializer_class _meta.model_class = model_class _meta.fields = yank_fields_from_attrs(output_fields, _as=Field) input_fields = yank_fields_from_attrs(input_fields, _as=InputField) super().__init_subclass_with_meta__( _meta=_meta, input_fields=input_fields, **options ) @classmethod def get_serializer_kwargs(cls, root, info, **input): lookup_field = cls._meta.lookup_field model_class = cls._meta.model_class if model_class: for input_dict_key, maybe_enum in input.items(): if isinstance(maybe_enum, Enum): input[input_dict_key] = maybe_enum.value if "update" in cls._meta.model_operations and lookup_field in input: instance = get_object_or_404( model_class, **{lookup_field: input[lookup_field]} ) partial = True elif "create" in cls._meta.model_operations: instance = None partial = False else: raise Exception( 'Invalid update operation. Input parameter "{}" required.'.format( lookup_field ) ) return { "instance": instance, "data": input, "context": {"request": info.context}, "partial": partial, } return {"data": input, "context": {"request": info.context}} @classmethod def mutate_and_get_payload(cls, root, info, **input): kwargs = cls.get_serializer_kwargs(root, info, **input) serializer = cls._meta.serializer_class(**kwargs) if serializer.is_valid(): return cls.perform_mutate(serializer, info) else: errors = ErrorType.from_errors(serializer.errors) return cls(errors=errors) @classmethod def perform_mutate(cls, serializer, info): obj = serializer.save() kwargs = {} for f, field in serializer.fields.items(): if not field.write_only: if isinstance(field, serializers.SerializerMethodField): kwargs[f] = field.to_representation(obj) else: kwargs[f] = field.get_attribute(obj) return cls(errors=None, **kwargs) django-graphene-3.2.3/graphene_django/rest_framework/serializer_converter.py000066400000000000000000000126161476451357100275520ustar00rootroot00000000000000from functools import singledispatch from django.core.exceptions import ImproperlyConfigured from rest_framework import serializers import graphene from ..converter import convert_choices_to_named_enum_with_descriptions from ..registry import get_global_registry from .types import DictType @singledispatch def get_graphene_type_from_serializer_field(field): raise ImproperlyConfigured( f"Don't know how to convert the serializer field {field} ({field.__class__}) " "to Graphene type" ) def convert_serializer_field( field, is_input=True, convert_choices_to_enum=True, force_optional=False ): """ Converts a django rest frameworks field to a graphql field and marks the field as required if we are creating an input type and the field itself is required """ if isinstance(field, serializers.ChoiceField) and not convert_choices_to_enum: graphql_type = graphene.String else: graphql_type = get_graphene_type_from_serializer_field(field) args = [] kwargs = { "description": field.help_text, "required": is_input and field.required and not force_optional, } # if it is a tuple or a list it means that we are returning # the graphql type and the child type if isinstance(graphql_type, (list, tuple)): kwargs["of_type"] = graphql_type[1] graphql_type = graphql_type[0] if isinstance(field, serializers.ModelSerializer): if is_input: graphql_type = convert_serializer_to_input_type(field.__class__) else: global_registry = get_global_registry() field_model = field.Meta.model args = [global_registry.get_type_for_model(field_model)] elif isinstance(field, serializers.ListSerializer): field = field.child if is_input: kwargs["of_type"] = convert_serializer_to_input_type(field.__class__) else: del kwargs["of_type"] global_registry = get_global_registry() field_model = field.Meta.model args = [global_registry.get_type_for_model(field_model)] return graphql_type(*args, **kwargs) def convert_serializer_to_input_type(serializer_class): cached_type = convert_serializer_to_input_type.cache.get( serializer_class.__name__, None ) if cached_type: return cached_type serializer = serializer_class() items = { name: convert_serializer_field(field) for name, field in serializer.fields.items() } ret_type = type( f"{serializer.__class__.__name__}Input", (graphene.InputObjectType,), items, ) convert_serializer_to_input_type.cache[serializer_class.__name__] = ret_type return ret_type convert_serializer_to_input_type.cache = {} @get_graphene_type_from_serializer_field.register(serializers.Field) def convert_serializer_field_to_string(field): return graphene.String @get_graphene_type_from_serializer_field.register(serializers.ModelSerializer) def convert_serializer_to_field(field): return graphene.Field @get_graphene_type_from_serializer_field.register(serializers.ListSerializer) def convert_list_serializer_to_field(field): child_type = get_graphene_type_from_serializer_field(field.child) return (graphene.List, child_type) @get_graphene_type_from_serializer_field.register(serializers.IntegerField) def convert_serializer_field_to_int(field): return graphene.Int @get_graphene_type_from_serializer_field.register(serializers.BooleanField) def convert_serializer_field_to_bool(field): return graphene.Boolean @get_graphene_type_from_serializer_field.register(serializers.FloatField) def convert_serializer_field_to_float(field): return graphene.Float @get_graphene_type_from_serializer_field.register(serializers.DecimalField) def convert_serializer_field_to_decimal(field): return graphene.Decimal @get_graphene_type_from_serializer_field.register(serializers.DateTimeField) def convert_serializer_field_to_datetime_time(field): return graphene.types.datetime.DateTime @get_graphene_type_from_serializer_field.register(serializers.DateField) def convert_serializer_field_to_date_time(field): return graphene.types.datetime.Date @get_graphene_type_from_serializer_field.register(serializers.TimeField) def convert_serializer_field_to_time(field): return graphene.types.datetime.Time @get_graphene_type_from_serializer_field.register(serializers.ListField) def convert_serializer_field_to_list(field, is_input=True): child_type = get_graphene_type_from_serializer_field(field.child) return (graphene.List, child_type) @get_graphene_type_from_serializer_field.register(serializers.DictField) def convert_serializer_field_to_dict(field): return DictType @get_graphene_type_from_serializer_field.register(serializers.JSONField) def convert_serializer_field_to_jsonstring(field): return graphene.types.json.JSONString @get_graphene_type_from_serializer_field.register(serializers.MultipleChoiceField) def convert_serializer_field_to_list_of_enum(field): child_type = convert_serializer_field_to_enum(field) return (graphene.List, child_type) @get_graphene_type_from_serializer_field.register(serializers.ChoiceField) def convert_serializer_field_to_enum(field): # enums require a name name = field.field_name or field.source or "Choices" return convert_choices_to_named_enum_with_descriptions(name, field.choices) django-graphene-3.2.3/graphene_django/rest_framework/tests/000077500000000000000000000000001476451357100240745ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/rest_framework/tests/__init__.py000066400000000000000000000000001476451357100261730ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/rest_framework/tests/test_field_converter.py000066400000000000000000000142251476451357100306630ustar00rootroot00000000000000import copy from django.db import models from pytest import raises from rest_framework import serializers import graphene from ..serializer_converter import convert_serializer_field from ..types import DictType def _get_type( rest_framework_field, is_input=True, convert_choices_to_enum=True, **kwargs ): # prevents the following error: # AssertionError: The `source` argument is not meaningful when applied to a `child=` field. # Remove `source=` from the field declaration. # since we are reusing the same child in when testing the required attribute if "child" in kwargs: kwargs["child"] = copy.deepcopy(kwargs["child"]) field = rest_framework_field(**kwargs) return convert_serializer_field( field, is_input=is_input, convert_choices_to_enum=convert_choices_to_enum ) def assert_conversion(rest_framework_field, graphene_field, **kwargs): graphene_type = _get_type( rest_framework_field, help_text="Custom Help Text", **kwargs ) assert isinstance(graphene_type, graphene_field) graphene_type_required = _get_type( rest_framework_field, help_text="Custom Help Text", required=True, **kwargs ) assert isinstance(graphene_type_required, graphene_field) return graphene_type def test_should_unknown_rest_framework_field_raise_exception(): with raises(Exception) as excinfo: convert_serializer_field(None) assert "Don't know how to convert the serializer field" in str(excinfo.value) def test_should_char_convert_string(): assert_conversion(serializers.CharField, graphene.String) def test_should_email_convert_string(): assert_conversion(serializers.EmailField, graphene.String) def test_should_slug_convert_string(): assert_conversion(serializers.SlugField, graphene.String) def test_should_url_convert_string(): assert_conversion(serializers.URLField, graphene.String) def test_should_choice_convert_enum(): field = assert_conversion( serializers.ChoiceField, graphene.Enum, choices=[("h", "Hello"), ("w", "World")], source="word", ) assert field._meta.enum.__members__["H"].value == "h" assert field._meta.enum.__members__["H"].description == "Hello" assert field._meta.enum.__members__["W"].value == "w" assert field._meta.enum.__members__["W"].description == "World" def test_should_choice_convert_string_if_enum_disabled(): assert_conversion( serializers.ChoiceField, graphene.String, choices=[("h", "Hello"), ("w", "World")], source="word", convert_choices_to_enum=False, ) def test_should_base_field_convert_string(): assert_conversion(serializers.Field, graphene.String) def test_should_regex_convert_string(): assert_conversion(serializers.RegexField, graphene.String, regex="[0-9]+") def test_should_uuid_convert_string(): assert_conversion(serializers.UUIDField, graphene.String) def test_should_model_convert_field(): class MyModelSerializer(serializers.ModelSerializer): class Meta: model = None fields = "__all__" assert_conversion(MyModelSerializer, graphene.Field, is_input=False) def test_should_date_time_convert_datetime(): assert_conversion(serializers.DateTimeField, graphene.types.datetime.DateTime) def test_should_date_convert_date(): assert_conversion(serializers.DateField, graphene.types.datetime.Date) def test_should_time_convert_time(): assert_conversion(serializers.TimeField, graphene.types.datetime.Time) def test_should_integer_convert_int(): assert_conversion(serializers.IntegerField, graphene.Int) def test_should_boolean_convert_boolean(): assert_conversion(serializers.BooleanField, graphene.Boolean) def test_should_float_convert_float(): assert_conversion(serializers.FloatField, graphene.Float) def test_should_decimal_convert_decimal(): assert_conversion( serializers.DecimalField, graphene.Decimal, max_digits=4, decimal_places=2 ) def test_should_list_convert_to_list(): class StringListField(serializers.ListField): child = serializers.CharField() field_a = assert_conversion( serializers.ListField, graphene.List, child=serializers.IntegerField(min_value=0, max_value=100), ) assert field_a.of_type == graphene.Int field_b = assert_conversion(StringListField, graphene.List) assert field_b.of_type == graphene.String def test_should_list_serializer_convert_to_list(): class FooModel(models.Model): pass class ChildSerializer(serializers.ModelSerializer): class Meta: model = FooModel fields = "__all__" class ParentSerializer(serializers.ModelSerializer): child = ChildSerializer(many=True) class Meta: model = FooModel fields = "__all__" converted_type = convert_serializer_field( ParentSerializer().get_fields()["child"], is_input=True ) assert isinstance(converted_type, graphene.List) converted_type = convert_serializer_field( ParentSerializer().get_fields()["child"], is_input=False ) assert isinstance(converted_type, graphene.List) assert converted_type.of_type is None def test_should_dict_convert_dict(): assert_conversion(serializers.DictField, DictType) def test_should_duration_convert_string(): assert_conversion(serializers.DurationField, graphene.String) def test_should_file_convert_string(): assert_conversion(serializers.FileField, graphene.String) def test_should_filepath_convert_string(): assert_conversion(serializers.FilePathField, graphene.Enum, path="/") def test_should_ip_convert_string(): assert_conversion(serializers.IPAddressField, graphene.String) def test_should_image_convert_string(): assert_conversion(serializers.ImageField, graphene.String) def test_should_json_convert_jsonstring(): assert_conversion(serializers.JSONField, graphene.types.json.JSONString) def test_should_multiplechoicefield_convert_to_list_of_enum(): field = assert_conversion( serializers.MultipleChoiceField, graphene.List, choices=[1, 2, 3] ) assert issubclass(field.of_type, graphene.Enum) django-graphene-3.2.3/graphene_django/rest_framework/tests/test_multiple_model_serializers.py000066400000000000000000000033101476451357100331310ustar00rootroot00000000000000from django.db import models from rest_framework import serializers import graphene from graphene import Schema from graphene_django import DjangoObjectType from graphene_django.rest_framework.mutation import SerializerMutation class MyFakeChildModel(models.Model): name = models.CharField(max_length=50) created = models.DateTimeField(auto_now_add=True) class MyFakeParentModel(models.Model): name = models.CharField(max_length=50) created = models.DateTimeField(auto_now_add=True) child1 = models.OneToOneField( MyFakeChildModel, related_name="parent1", on_delete=models.CASCADE ) child2 = models.OneToOneField( MyFakeChildModel, related_name="parent2", on_delete=models.CASCADE ) class ParentType(DjangoObjectType): class Meta: model = MyFakeParentModel interfaces = (graphene.relay.Node,) fields = "__all__" class ChildType(DjangoObjectType): class Meta: model = MyFakeChildModel interfaces = (graphene.relay.Node,) fields = "__all__" class MyModelChildSerializer(serializers.ModelSerializer): class Meta: model = MyFakeChildModel fields = "__all__" class MyModelParentSerializer(serializers.ModelSerializer): child1 = MyModelChildSerializer() child2 = MyModelChildSerializer() class Meta: model = MyFakeParentModel fields = "__all__" class MyParentModelMutation(SerializerMutation): class Meta: serializer_class = MyModelParentSerializer class Mutation(graphene.ObjectType): createParentWithChild = MyParentModelMutation.Field() def test_create_schema(): schema = Schema(mutation=Mutation, types=[ParentType, ChildType]) assert schema django-graphene-3.2.3/graphene_django/rest_framework/tests/test_mutation.py000066400000000000000000000246051476451357100273540ustar00rootroot00000000000000import datetime from pytest import raises from rest_framework import serializers from graphene import Field, ResolveInfo, String from graphene.types.inputobjecttype import InputObjectType from ...types import DjangoObjectType from ..models import ( MyFakeModel, MyFakeModelWithChoiceField, MyFakeModelWithDate, MyFakeModelWithPassword, ) from ..mutation import SerializerMutation def mock_info(): return ResolveInfo( None, None, None, None, path=None, schema=None, fragments=None, root_value=None, operation=None, variable_values=None, context=None, is_awaitable=None, ) class MyModelSerializer(serializers.ModelSerializer): class Meta: model = MyFakeModel fields = "__all__" class MyModelSerializerWithMethod(serializers.ModelSerializer): days_since_last_edit = serializers.SerializerMethodField() class Meta: model = MyFakeModelWithDate fields = "__all__" def get_days_since_last_edit(self, obj): now = datetime.date(2020, 1, 8) return (now - obj.last_edited).days class MyModelMutation(SerializerMutation): class Meta: serializer_class = MyModelSerializer class MySerializer(serializers.Serializer): text = serializers.CharField() model = MyModelSerializer() def create(self, validated_data): return validated_data def test_needs_serializer_class(): with raises(Exception) as exc: class MyMutation(SerializerMutation): pass assert str(exc.value) == "serializer_class is required for the SerializerMutation" def test_has_fields(): class MyMutation(SerializerMutation): class Meta: serializer_class = MySerializer assert "text" in MyMutation._meta.fields assert "model" in MyMutation._meta.fields assert "errors" in MyMutation._meta.fields def test_has_input_fields(): class MyMutation(SerializerMutation): class Meta: serializer_class = MySerializer assert "text" in MyMutation.Input._meta.fields assert "model" in MyMutation.Input._meta.fields def test_exclude_fields(): class MyMutation(SerializerMutation): class Meta: serializer_class = MyModelSerializer exclude_fields = ["created"] assert "cool_name" in MyMutation._meta.fields assert "created" not in MyMutation._meta.fields assert "errors" in MyMutation._meta.fields assert "cool_name" in MyMutation.Input._meta.fields assert "created" not in MyMutation.Input._meta.fields def test_model_serializer_optional_fields(): class MyMutation(SerializerMutation): class Meta: serializer_class = MyModelSerializer optional_fields = ("cool_name",) assert "cool_name" in MyMutation.Input._meta.fields assert MyMutation.Input._meta.fields["cool_name"].type == String def test_write_only_field(): class WriteOnlyFieldModelSerializer(serializers.ModelSerializer): password = serializers.CharField(write_only=True) class Meta: model = MyFakeModelWithPassword fields = ["cool_name", "password"] class MyMutation(SerializerMutation): class Meta: serializer_class = WriteOnlyFieldModelSerializer result = MyMutation.mutate_and_get_payload( None, mock_info(), **{"cool_name": "New Narf", "password": "admin"} ) assert hasattr(result, "cool_name") assert not hasattr( result, "password" ), "'password' is write_only field and shouldn't be visible" def test_write_only_field_using_extra_kwargs(): class WriteOnlyFieldModelSerializer(serializers.ModelSerializer): class Meta: model = MyFakeModelWithPassword fields = ["cool_name", "password"] extra_kwargs = {"password": {"write_only": True}} class MyMutation(SerializerMutation): class Meta: serializer_class = WriteOnlyFieldModelSerializer result = MyMutation.mutate_and_get_payload( None, mock_info(), **{"cool_name": "New Narf", "password": "admin"} ) assert hasattr(result, "cool_name") assert not hasattr( result, "password" ), "'password' is write_only field and shouldn't be visible" def test_read_only_fields(): class ReadOnlyFieldModelSerializer(serializers.ModelSerializer): id = serializers.CharField(read_only=True) cool_name = serializers.CharField(read_only=True) class Meta: model = MyFakeModelWithPassword lookup_field = "id" fields = ["id", "cool_name", "password"] class MyMutation(SerializerMutation): class Meta: serializer_class = ReadOnlyFieldModelSerializer assert "password" in MyMutation.Input._meta.fields assert "id" in MyMutation.Input._meta.fields assert ( "cool_name" not in MyMutation.Input._meta.fields ), "'cool_name' is read_only field and shouldn't be on arguments" def test_hidden_fields(): class SerializerWithHiddenField(serializers.Serializer): cool_name = serializers.CharField() user = serializers.HiddenField(default=serializers.CurrentUserDefault()) class MyMutation(SerializerMutation): class Meta: serializer_class = SerializerWithHiddenField assert "cool_name" in MyMutation.Input._meta.fields assert ( "user" not in MyMutation.Input._meta.fields ), "'user' is hidden field and shouldn't be on arguments" def test_nested_model(): class MyFakeModelGrapheneType(DjangoObjectType): class Meta: model = MyFakeModel fields = "__all__" class MyMutation(SerializerMutation): class Meta: serializer_class = MySerializer model_field = MyMutation._meta.fields["model"] assert isinstance(model_field, Field) assert model_field.type == MyFakeModelGrapheneType model_input = MyMutation.Input._meta.fields["model"] model_input_type = model_input._type.of_type assert issubclass(model_input_type, InputObjectType) assert "cool_name" in model_input_type._meta.fields assert "created" in model_input_type._meta.fields def test_mutate_and_get_payload_success(): class MyMutation(SerializerMutation): class Meta: serializer_class = MySerializer result = MyMutation.mutate_and_get_payload( None, mock_info(), **{"text": "value", "model": {"cool_name": "other_value"}} ) assert result.errors is None def test_model_add_mutate_and_get_payload_success(): result = MyModelMutation.mutate_and_get_payload( None, mock_info(), **{"cool_name": "Narf"} ) assert result.errors is None assert result.cool_name == "Narf" assert isinstance(result.created, datetime.datetime) def test_model_update_mutate_and_get_payload_success(): instance = MyFakeModel.objects.create(cool_name="Narf") result = MyModelMutation.mutate_and_get_payload( None, mock_info(), **{"id": instance.id, "cool_name": "New Narf"} ) assert result.errors is None assert result.cool_name == "New Narf" def test_model_partial_update_mutate_and_get_payload_success(): instance = MyFakeModel.objects.create(cool_name="Narf") result = MyModelMutation.mutate_and_get_payload( None, mock_info(), **{"id": instance.id} ) assert result.errors is None assert result.cool_name == "Narf" def test_model_invalid_update_mutate_and_get_payload_success(): class InvalidModelMutation(SerializerMutation): class Meta: serializer_class = MyModelSerializer model_operations = ["update"] with raises(Exception) as exc: InvalidModelMutation.mutate_and_get_payload( None, mock_info(), **{"cool_name": "Narf"} ) assert '"id" required' in str(exc.value) def test_perform_mutate_success(): class MyMethodMutation(SerializerMutation): class Meta: serializer_class = MyModelSerializerWithMethod result = MyMethodMutation.mutate_and_get_payload( None, mock_info(), **{"cool_name": "Narf", "last_edited": datetime.date(2020, 1, 4)}, ) assert result.errors is None assert result.cool_name == "Narf" assert result.days_since_last_edit == 4 def test_perform_mutate_success_with_enum_choice_field(): class ListViewChoiceFieldSerializer(serializers.ModelSerializer): choice_type = serializers.ChoiceField( choices=[(x.name, x.value) for x in MyFakeModelWithChoiceField.ChoiceType], required=False, ) class Meta: model = MyFakeModelWithChoiceField fields = "__all__" class SomeCreateSerializerMutation(SerializerMutation): class Meta: serializer_class = ListViewChoiceFieldSerializer choice_type = { "choice_type": SomeCreateSerializerMutation.Input.choice_type.type.get("ASDF") } name = MyFakeModelWithChoiceField.ChoiceType.ASDF.name result = SomeCreateSerializerMutation.mutate_and_get_payload( None, mock_info(), **choice_type ) assert result.errors is None assert result.choice_type == name kwargs = SomeCreateSerializerMutation.get_serializer_kwargs( None, mock_info(), **choice_type ) assert kwargs["data"]["choice_type"] == name assert 1 == MyFakeModelWithChoiceField.objects.count() item = MyFakeModelWithChoiceField.objects.first() assert item.choice_type == name def test_mutate_and_get_payload_error(): class MyMutation(SerializerMutation): class Meta: serializer_class = MySerializer # missing required fields result = MyMutation.mutate_and_get_payload(None, mock_info(), **{}) assert len(result.errors) > 0 def test_model_mutate_and_get_payload_error(): # missing required fields result = MyModelMutation.mutate_and_get_payload(None, mock_info(), **{}) assert len(result.errors) > 0 def test_mutation_error_camelcased(graphene_settings): graphene_settings.CAMELCASE_ERRORS = True result = MyModelMutation.mutate_and_get_payload(None, mock_info(), **{}) assert result.errors[0].field == "coolName" def test_invalid_serializer_operations(): with raises(Exception) as exc: class MyModelMutation(SerializerMutation): class Meta: serializer_class = MyModelSerializer model_operations = ["Add"] assert "model_operations" in str(exc.value) django-graphene-3.2.3/graphene_django/rest_framework/types.py000066400000000000000000000002421476451357100244460ustar00rootroot00000000000000import graphene from graphene.types.unmountedtype import UnmountedType class DictType(UnmountedType): key = graphene.String() value = graphene.String() django-graphene-3.2.3/graphene_django/settings.py000066400000000000000000000113471476451357100221200ustar00rootroot00000000000000""" Settings for Graphene are all namespaced in the GRAPHENE setting. For example your project's `settings.py` file might look like this: GRAPHENE = { 'SCHEMA': 'my_app.schema.schema' 'MIDDLEWARE': ( 'graphene_django.debug.DjangoDebugMiddleware', ) } This module provides the `graphene_settings` object, that is used to access Graphene settings, checking for user settings first, then falling back to the defaults. """ import importlib # Available in Python 3.1+ from django.conf import settings from django.test.signals import setting_changed # Copied shamelessly from Django REST Framework DEFAULTS = { "SCHEMA": None, "SCHEMA_OUTPUT": "schema.json", "SCHEMA_INDENT": 2, "MIDDLEWARE": (), # Set to True if the connection fields must have # either the first or last argument "RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST": False, # Max items returned in ConnectionFields / FilterConnectionFields "RELAY_CONNECTION_MAX_LIMIT": 100, "CAMELCASE_ERRORS": True, # Automatically convert Choice fields of Django into Enum fields "DJANGO_CHOICE_FIELD_ENUM_CONVERT": True, # Set to True to enable v2 naming convention for choice field Enum's "DJANGO_CHOICE_FIELD_ENUM_V2_NAMING": False, "DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME": None, # Use a separate path for handling subscriptions. "SUBSCRIPTION_PATH": None, # By default GraphiQL headers editor tab is enabled, set to False to hide it # This sets headerEditorEnabled GraphiQL option, for details go to # https://github.com/graphql/graphiql/tree/main/packages/graphiql#options "GRAPHIQL_HEADER_EDITOR_ENABLED": True, "GRAPHIQL_SHOULD_PERSIST_HEADERS": False, "GRAPHIQL_INPUT_VALUE_DEPRECATION": False, "ATOMIC_MUTATIONS": False, "TESTING_ENDPOINT": "/graphql", "MAX_VALIDATION_ERRORS": None, } if settings.DEBUG: DEFAULTS["MIDDLEWARE"] += ("graphene_django.debug.DjangoDebugMiddleware",) # List of settings that may be in string import notation. IMPORT_STRINGS = ("MIDDLEWARE", "SCHEMA") def perform_import(val, setting_name): """ If the given setting is a string import notation, then perform the necessary import or imports. """ if val is None: return None elif isinstance(val, str): return import_from_string(val, setting_name) elif isinstance(val, (list, tuple)): return [import_from_string(item, setting_name) for item in val] return val def import_from_string(val, setting_name): """ Attempt to import a class from a string representation. """ try: # Nod to tastypie's use of importlib. parts = val.split(".") module_path, class_name = ".".join(parts[:-1]), parts[-1] module = importlib.import_module(module_path) return getattr(module, class_name) except (ImportError, AttributeError) as e: msg = "Could not import '{}' for Graphene setting '{}'. {}: {}.".format( val, setting_name, e.__class__.__name__, e, ) raise ImportError(msg) class GrapheneSettings: """ A settings object, that allows API settings to be accessed as properties. For example: from graphene_django.settings import settings print(settings.SCHEMA) Any setting with string import paths will be automatically resolved and return the class, rather than the string literal. """ def __init__(self, user_settings=None, defaults=None, import_strings=None): if user_settings: self._user_settings = user_settings self.defaults = defaults or DEFAULTS self.import_strings = import_strings or IMPORT_STRINGS @property def user_settings(self): if not hasattr(self, "_user_settings"): self._user_settings = getattr(settings, "GRAPHENE", {}) return self._user_settings def __getattr__(self, attr): if attr not in self.defaults: raise AttributeError("Invalid Graphene setting: '%s'" % attr) try: # Check if present in user settings val = self.user_settings[attr] except KeyError: # Fall back to defaults val = self.defaults[attr] # Coerce import strings into classes if attr in self.import_strings: val = perform_import(val, attr) # Cache the result setattr(self, attr, val) return val graphene_settings = GrapheneSettings(None, DEFAULTS, IMPORT_STRINGS) def reload_graphene_settings(*args, **kwargs): global graphene_settings setting, value = kwargs["setting"], kwargs["value"] if setting == "GRAPHENE": graphene_settings = GrapheneSettings(value, DEFAULTS, IMPORT_STRINGS) setting_changed.connect(reload_graphene_settings) django-graphene-3.2.3/graphene_django/static/000077500000000000000000000000001476451357100211675ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/static/graphene_django/000077500000000000000000000000001476451357100243025ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/static/graphene_django/graphiql.js000066400000000000000000000077051476451357100264600ustar00rootroot00000000000000(function ( document, GRAPHENE_SETTINGS, GraphiQL, React, ReactDOM, graphqlWs, GraphiQLPluginExplorer, fetch, history, location, ) { // Collect the URL parameters var parameters = {}; location.hash .substr(1) .split("&") .forEach(function (entry) { var eq = entry.indexOf("="); if (eq >= 0) { parameters[decodeURIComponent(entry.slice(0, eq))] = decodeURIComponent( entry.slice(eq + 1), ); } }); // Produce a Location fragment string from a parameter object. function locationQuery(params) { return ( "#" + Object.keys(params) .map(function (key) { return ( encodeURIComponent(key) + "=" + encodeURIComponent(params[key]) ); }) .join("&") ); } // Derive a fetch URL from the current URL, sans the GraphQL parameters. var graphqlParamNames = { query: true, variables: true, operationName: true, }; var otherParams = {}; for (var k in parameters) { if (parameters.hasOwnProperty(k) && graphqlParamNames[k] !== true) { otherParams[k] = parameters[k]; } } var fetchURL = locationQuery(otherParams); // Derive the subscription URL. If the SUBSCRIPTION_URL setting is specified, uses that value. Otherwise // assumes the current window location with an appropriate websocket protocol. var subscribeURL = location.origin.replace(/^http/, "ws") + (GRAPHENE_SETTINGS.subscriptionPath || location.pathname); function trueLambda() { return true; }; var headers = {}; var cookies = ("; " + document.cookie).split("; csrftoken="); if (cookies.length == 2) { csrftoken = cookies.pop().split(";").shift(); } else { csrftoken = document.querySelector("[name=csrfmiddlewaretoken]").value; } if (csrftoken) { headers['X-CSRFToken'] = csrftoken } var graphQLFetcher = GraphiQL.createFetcher({ url: fetchURL, wsClient: graphqlWs.createClient({ url: subscribeURL, shouldRetry: trueLambda, lazy: true, }), headers: headers }) // When the query and variables string is edited, update the URL bar so // that it can be easily shared. function onEditQuery(newQuery) { parameters.query = newQuery; updateURL(); } function onEditVariables(newVariables) { parameters.variables = newVariables; updateURL(); } function onEditOperationName(newOperationName) { parameters.operationName = newOperationName; updateURL(); } function updateURL() { history.replaceState(null, null, locationQuery(parameters)); } function GraphiQLWithExplorer() { var [query, setQuery] = React.useState(parameters.query); function handleQuery(query) { setQuery(query); onEditQuery(query); } var explorerPlugin = GraphiQLPluginExplorer.useExplorerPlugin({ query: query, onEdit: handleQuery, }); var options = { fetcher: graphQLFetcher, plugins: [explorerPlugin], defaultEditorToolsVisibility: true, onEditQuery: handleQuery, onEditVariables: onEditVariables, onEditOperationName: onEditOperationName, isHeadersEditorEnabled: GRAPHENE_SETTINGS.graphiqlHeaderEditorEnabled, shouldPersistHeaders: GRAPHENE_SETTINGS.graphiqlShouldPersistHeaders, inputValueDeprecation: GRAPHENE_SETTINGS.graphiqlInputValueDeprecation, query: query, }; if (parameters.variables) { options.variables = parameters.variables; } if (parameters.operation_name) { options.operationName = parameters.operation_name; } return React.createElement(GraphiQL, options); } // Render into the body. ReactDOM.render( React.createElement(GraphiQLWithExplorer), document.getElementById("editor"), ); })( document, window.GRAPHENE_SETTINGS, window.GraphiQL, window.React, window.ReactDOM, window.graphqlWs, window.GraphiQLPluginExplorer, window.fetch, window.history, window.location, ); django-graphene-3.2.3/graphene_django/templates/000077500000000000000000000000001476451357100216765ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/templates/graphene/000077500000000000000000000000001476451357100234675ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/templates/graphene/graphiql.html000066400000000000000000000052361476451357100261720ustar00rootroot00000000000000 {% load static %}
{% csrf_token %} django-graphene-3.2.3/graphene_django/tests/000077500000000000000000000000001476451357100210425ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/tests/__init__.py000066400000000000000000000000001476451357100231410ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/tests/forms.py000066400000000000000000000005351476451357100225450ustar00rootroot00000000000000from django import forms from django.core.exceptions import ValidationError from .models import Pet class PetForm(forms.ModelForm): class Meta: model = Pet fields = "__all__" def clean_age(self): age = self.cleaned_data["age"] if age >= 99: raise ValidationError("Too old") return age django-graphene-3.2.3/graphene_django/tests/issues/000077500000000000000000000000001476451357100223555ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/tests/issues/__init__.py000066400000000000000000000000001476451357100244540ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/tests/issues/test_520.py000066400000000000000000000016321476451357100242760ustar00rootroot00000000000000# https://github.com/graphql-python/graphene-django/issues/520 from django import forms from rest_framework import serializers import graphene from ...forms.mutation import DjangoFormMutation from ...rest_framework.models import MyFakeModel from ...rest_framework.mutation import SerializerMutation class MyModelSerializer(serializers.ModelSerializer): class Meta: model = MyFakeModel fields = "__all__" class MyForm(forms.Form): text = forms.CharField() def test_can_use_form_and_serializer_mutations(): class MyMutation(SerializerMutation): class Meta: serializer_class = MyModelSerializer class MyFormMutation(DjangoFormMutation): class Meta: form_class = MyForm class Mutation(graphene.ObjectType): my_mutation = MyMutation.Field() my_form_mutation = MyFormMutation.Field() graphene.Schema(mutation=Mutation) django-graphene-3.2.3/graphene_django/tests/models.py000066400000000000000000000115041476451357100227000ustar00rootroot00000000000000import django from django.db import models from django.utils.translation import gettext_lazy as _ CHOICES = ((1, "this"), (2, _("that"))) def get_choices_as_class(choices_class): if django.VERSION >= (5, 0): return choices_class else: return choices_class.choices def get_choices_as_callable(choices_class): if django.VERSION >= (5, 0): def choices(): return choices_class.choices return choices else: return choices_class.choices class TypedIntChoice(models.IntegerChoices): CHOICE_THIS = 1 CHOICE_THAT = 2 class TypedStrChoice(models.TextChoices): CHOICE_THIS = "this" CHOICE_THAT = "that" class Person(models.Model): name = models.CharField(max_length=30) parent = models.ForeignKey( "self", on_delete=models.CASCADE, null=True, blank=True, related_name="children" ) class Pet(models.Model): name = models.CharField(max_length=30) age = models.PositiveIntegerField() owner = models.ForeignKey( "Person", on_delete=models.CASCADE, null=True, blank=True, related_name="pets" ) class FilmDetails(models.Model): location = models.CharField(max_length=30) film = models.OneToOneField( "Film", on_delete=models.CASCADE, related_name="details", null=True, blank=True, ) class Film(models.Model): genre = models.CharField( max_length=2, help_text="Genre", choices=[("do", "Documentary"), ("ac", "Action"), ("ot", "Other")], default="ot", ) reporters = models.ManyToManyField("Reporter", related_name="films") class DoeReporterManager(models.Manager): def get_queryset(self): return super().get_queryset().filter(last_name="Doe") class Reporter(models.Model): first_name = models.CharField(max_length=30) last_name = models.CharField(max_length=30) email = models.EmailField() pets = models.ManyToManyField("self") a_choice = models.IntegerField(choices=CHOICES, null=True, blank=True) typed_choice = models.IntegerField( choices=TypedIntChoice.choices, null=True, blank=True, ) class_choice = models.IntegerField( choices=get_choices_as_class(TypedIntChoice), null=True, blank=True, ) callable_choice = models.IntegerField( choices=get_choices_as_callable(TypedStrChoice), null=True, blank=True, ) objects = models.Manager() doe_objects = DoeReporterManager() fans = models.ManyToManyField(Person) reporter_type = models.IntegerField( "Reporter Type", null=True, blank=True, choices=[(1, "Regular"), (2, "CNN Reporter")], ) def __str__(self): # __unicode__ on Python 2 return f"{self.first_name} {self.last_name}" def __init__(self, *args, **kwargs): """ Override the init method so that during runtime, Django can know that this object can be a CNNReporter by casting it to the proxy model. Otherwise, as far as Django knows, when a CNNReporter is pulled from the database, it is still of type Reporter. This was added to test proxy model support. """ super().__init__(*args, **kwargs) if self.reporter_type == 2: # quick and dirty way without enums self.__class__ = CNNReporter def some_method(self): return 123 class CNNReporterManager(models.Manager): def get_queryset(self): return super().get_queryset().filter(reporter_type=2) class CNNReporter(Reporter): """ This class is a proxy model for Reporter, used for testing proxy model support """ class Meta: proxy = True objects = CNNReporterManager() class APNewsReporter(Reporter): """ This class only inherits from Reporter for testing multi table inheritance similar to what you'd see in django-polymorphic """ alias = models.CharField(max_length=30) objects = models.Manager() class Article(models.Model): headline = models.CharField(max_length=100) pub_date = models.DateField(auto_now_add=True) pub_date_time = models.DateTimeField(auto_now_add=True) reporter = models.ForeignKey( Reporter, on_delete=models.CASCADE, related_name="articles" ) editor = models.ForeignKey( Reporter, on_delete=models.CASCADE, related_name="edited_articles_+" ) lang = models.CharField( max_length=2, help_text="Language", choices=[("es", "Spanish"), ("en", "English")], default="es", ) importance = models.IntegerField( "Importance", null=True, blank=True, choices=[(1, "Very important"), (2, "Not as important")], ) def __str__(self): # __unicode__ on Python 2 return self.headline class Meta: ordering = ("headline",) django-graphene-3.2.3/graphene_django/tests/mutations.py000066400000000000000000000005701476451357100234410ustar00rootroot00000000000000from graphene import Field from graphene_django.forms.mutation import DjangoFormMutation, DjangoModelFormMutation from .forms import PetForm from .types import PetType class PetFormMutation(DjangoFormMutation): class Meta: form_class = PetForm class PetMutation(DjangoModelFormMutation): pet = Field(PetType) class Meta: form_class = PetForm django-graphene-3.2.3/graphene_django/tests/schema.py000066400000000000000000000014241476451357100226550ustar00rootroot00000000000000import graphene from graphene import Schema, relay from ..types import DjangoObjectType from .models import Article, Reporter class Character(DjangoObjectType): class Meta: model = Reporter interfaces = (relay.Node,) fields = "__all__" def get_node(self, info, id): pass class Human(DjangoObjectType): raises = graphene.String() class Meta: model = Article interfaces = (relay.Node,) fields = "__all__" def resolve_raises(self, info): raise Exception("This field should raise exception") def get_node(self, info, id): pass class Query(graphene.ObjectType): human = graphene.Field(Human) def resolve_human(self, info): return Human() schema = Schema(query=Query) django-graphene-3.2.3/graphene_django/tests/schema_view.py000066400000000000000000000014541476451357100237120ustar00rootroot00000000000000import graphene from graphene import ObjectType, Schema from .mutations import PetFormMutation, PetMutation class QueryRoot(ObjectType): thrower = graphene.String(required=True) request = graphene.String(required=True) test = graphene.String(who=graphene.String()) def resolve_thrower(self, info): raise Exception("Throws!") def resolve_request(self, info): return info.context.GET.get("q") def resolve_test(self, info, who=None): return "Hello %s" % (who or "World") class MutationRoot(ObjectType): pet_form_mutation = PetFormMutation.Field() pet_mutation = PetMutation.Field() write_test = graphene.Field(QueryRoot) def resolve_write_test(self, info): return QueryRoot() schema = Schema(query=QueryRoot, mutation=MutationRoot) django-graphene-3.2.3/graphene_django/tests/test_command.py000066400000000000000000000032151476451357100240720ustar00rootroot00000000000000from io import StringIO from textwrap import dedent from unittest.mock import mock_open, patch from django.core import management from graphene import ObjectType, Schema, String @patch("graphene_django.management.commands.graphql_schema.Command.save_json_file") def test_generate_json_file_on_call_graphql_schema(savefile_mock): out = StringIO() management.call_command("graphql_schema", schema="", stdout=out) assert "Successfully dumped GraphQL schema to schema.json" in out.getvalue() @patch("json.dump") def test_json_files_are_canonical(dump_mock): open_mock = mock_open() with patch("graphene_django.management.commands.graphql_schema.open", open_mock): management.call_command("graphql_schema", schema="") open_mock.assert_called_once() dump_mock.assert_called_once() assert dump_mock.call_args[1][ "sort_keys" ], "json.mock() should be used to sort the output" assert ( dump_mock.call_args[1]["indent"] > 0 ), "output should be pretty-printed by default" def test_generate_graphql_file_on_call_graphql_schema(): class Query(ObjectType): hi = String() mock_schema = Schema(query=Query) open_mock = mock_open() with patch("graphene_django.management.commands.graphql_schema.open", open_mock): management.call_command( "graphql_schema", schema=mock_schema, out="schema.graphql" ) open_mock.assert_called_once() handle = open_mock() handle.write.assert_called_once() schema_output = handle.write.call_args[0][0] assert schema_output == dedent( """\ type Query { hi: String }""" ) django-graphene-3.2.3/graphene_django/tests/test_converter.py000066400000000000000000000415231476451357100244670ustar00rootroot00000000000000from collections import namedtuple import pytest from django.db import models from django.utils.translation import gettext_lazy as _ from pytest import raises import graphene from graphene import NonNull from graphene.relay import ConnectionField, Node from graphene.types.datetime import Date, DateTime, Time from graphene.types.json import JSONString from graphene.types.scalars import BigInt from ..compat import ( ArrayField, HStoreField, MissingType, RangeField, ) from ..converter import ( convert_django_field, convert_django_field_with_choices, generate_enum_name, ) from ..registry import Registry from ..types import DjangoObjectType from .models import Article, Film, FilmDetails, Reporter, TypedIntChoice, TypedStrChoice # from graphene.core.types.custom_scalars import DateTime, Time, JSONString def assert_conversion(django_field, graphene_field, *args, **kwargs): _kwargs = {**kwargs, "help_text": "Custom Help Text"} if "null" not in kwargs: _kwargs["null"] = True field = django_field(*args, **_kwargs) graphene_type = convert_django_field(field) assert isinstance(graphene_type, graphene_field) field = graphene_type.Field() assert field.description == "Custom Help Text" _kwargs = kwargs.copy() if "null" not in kwargs: _kwargs["null"] = False nonnull_field = django_field(*args, **_kwargs) if not nonnull_field.null: nonnull_graphene_type = convert_django_field(nonnull_field) nonnull_field = nonnull_graphene_type.Field() assert isinstance(nonnull_field.type, graphene.NonNull) return nonnull_field return field def test_should_unknown_django_field_raise_exception(): with raises(Exception, match="Don't know how to convert the Django field"): convert_django_field(None) def test_should_date_time_convert_string(): assert_conversion(models.DateTimeField, DateTime) def test_should_date_convert_string(): assert_conversion(models.DateField, Date) def test_should_time_convert_string(): assert_conversion(models.TimeField, Time) def test_should_char_convert_string(): assert_conversion(models.CharField, graphene.String) def test_should_text_convert_string(): assert_conversion(models.TextField, graphene.String) def test_should_email_convert_string(): assert_conversion(models.EmailField, graphene.String) def test_should_slug_convert_string(): assert_conversion(models.SlugField, graphene.String) def test_should_url_convert_string(): assert_conversion(models.URLField, graphene.String) def test_should_ipaddress_convert_string(): assert_conversion(models.GenericIPAddressField, graphene.String) def test_should_file_convert_string(): assert_conversion(models.FileField, graphene.String) def test_should_image_convert_string(): assert_conversion(models.ImageField, graphene.String) def test_should_file_path_field_convert_string(): assert_conversion(models.FilePathField, graphene.String) def test_should_auto_convert_id(): assert_conversion(models.AutoField, graphene.ID, primary_key=True) def test_should_big_auto_convert_id(): assert_conversion(models.BigAutoField, graphene.ID, primary_key=True) def test_should_small_auto_convert_id(): assert_conversion(models.SmallAutoField, graphene.ID, primary_key=True) def test_should_uuid_convert_id(): assert_conversion(models.UUIDField, graphene.UUID) def test_should_auto_convert_duration(): assert_conversion(models.DurationField, graphene.Float) def test_should_positive_integer_convert_int(): assert_conversion(models.PositiveIntegerField, graphene.Int) def test_should_positive_small_convert_int(): assert_conversion(models.PositiveSmallIntegerField, graphene.Int) def test_should_small_integer_convert_int(): assert_conversion(models.SmallIntegerField, graphene.Int) def test_should_big_integer_convert_big_int(): assert_conversion(models.BigIntegerField, BigInt) def test_should_integer_convert_int(): assert_conversion(models.IntegerField, graphene.Int) def test_should_boolean_convert_boolean(): assert_conversion(models.BooleanField, graphene.Boolean, null=True) def test_should_boolean_convert_non_null_boolean(): field = assert_conversion(models.BooleanField, graphene.Boolean, null=False) assert isinstance(field.type, graphene.NonNull) assert field.type.of_type == graphene.Boolean def test_should_nullboolean_convert_boolean(): assert_conversion(models.NullBooleanField, graphene.Boolean) def test_field_with_choices_convert_enum(): field = models.CharField( help_text="Language", choices=(("es", "Spanish"), ("en", "English")) ) class ChoicesModel(models.Model): language = field class Meta: app_label = "test" graphene_type = convert_django_field_with_choices(field).type.of_type assert graphene_type._meta.name == "TestChoicesModelLanguageChoices" assert graphene_type._meta.enum.__members__["ES"].value == "es" assert graphene_type._meta.enum.__members__["ES"].description == "Spanish" assert graphene_type._meta.enum.__members__["EN"].value == "en" assert graphene_type._meta.enum.__members__["EN"].description == "English" def test_field_with_callable_choices_convert_enum(): def get_choices(): return ("es", "Spanish"), ("en", "English") field = models.CharField(help_text="Language", choices=get_choices) class CallableChoicesModel(models.Model): language = field class Meta: app_label = "test" graphene_type = convert_django_field_with_choices(field).type.of_type assert graphene_type._meta.name == "TestCallableChoicesModelLanguageChoices" assert graphene_type._meta.enum.__members__["ES"].value == "es" assert graphene_type._meta.enum.__members__["ES"].description == "Spanish" assert graphene_type._meta.enum.__members__["EN"].value == "en" assert graphene_type._meta.enum.__members__["EN"].description == "English" def test_field_with_grouped_choices(): field = models.CharField( help_text="Language", choices=(("Europe", (("es", "Spanish"), ("en", "English"))),), ) class GroupedChoicesModel(models.Model): language = field class Meta: app_label = "test" convert_django_field_with_choices(field) def test_field_with_choices_gettext(): field = models.CharField( help_text="Language", choices=(("es", _("Spanish")), ("en", _("English"))) ) class TranslatedChoicesModel(models.Model): language = field class Meta: app_label = "test" convert_django_field_with_choices(field) def test_field_with_choices_collision(): field = models.CharField( help_text="Timezone", choices=( ("Etc/GMT+1+2", "Fake choice to produce double collision"), ("Etc/GMT+1", "Greenwich Mean Time +1"), ("Etc/GMT-1", "Greenwich Mean Time -1"), ), ) class CollisionChoicesModel(models.Model): timezone = field class Meta: app_label = "test" convert_django_field_with_choices(field) def test_field_with_choices_convert_enum_false(): field = models.CharField( help_text="Language", choices=(("es", "Spanish"), ("en", "English")) ) class TranslatedModel(models.Model): language = field class Meta: app_label = "test" graphene_type = convert_django_field_with_choices( field, convert_choices_to_enum=False ) assert isinstance(graphene_type, graphene.String) def test_should_float_convert_float(): assert_conversion(models.FloatField, graphene.Float) def test_should_float_convert_decimal(): assert_conversion(models.DecimalField, graphene.Decimal) def test_should_manytomany_convert_connectionorlist(): registry = Registry() dynamic_field = convert_django_field(Reporter._meta.local_many_to_many[0], registry) assert not dynamic_field.get_type() def test_should_manytomany_convert_connectionorlist_list(): class A(DjangoObjectType): class Meta: model = Reporter fields = "__all__" graphene_field = convert_django_field( Reporter._meta.local_many_to_many[0], A._meta.registry ) assert isinstance(graphene_field, graphene.Dynamic) dynamic_field = graphene_field.get_type() assert isinstance(dynamic_field, graphene.Field) # A NonNull List of NonNull A ([A!]!) # https://github.com/graphql-python/graphene-django/issues/448 assert isinstance(dynamic_field.type, NonNull) assert isinstance(dynamic_field.type.of_type, graphene.List) assert isinstance(dynamic_field.type.of_type.of_type, NonNull) assert dynamic_field.type.of_type.of_type.of_type == A def test_should_manytomany_convert_connectionorlist_connection(): class A(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" graphene_field = convert_django_field( Reporter._meta.local_many_to_many[0], A._meta.registry ) assert isinstance(graphene_field, graphene.Dynamic) dynamic_field = graphene_field.get_type() assert isinstance(dynamic_field, ConnectionField) assert dynamic_field.type.of_type == A._meta.connection def test_should_manytoone_convert_connectionorlist(): class A(DjangoObjectType): class Meta: model = Article fields = "__all__" graphene_field = convert_django_field(Reporter.articles.rel, A._meta.registry) assert isinstance(graphene_field, graphene.Dynamic) dynamic_field = graphene_field.get_type() assert isinstance(dynamic_field, graphene.Field) # a NonNull List of NonNull A ([A!]!) assert isinstance(dynamic_field.type, NonNull) assert isinstance(dynamic_field.type.of_type, graphene.List) assert isinstance(dynamic_field.type.of_type.of_type, NonNull) assert dynamic_field.type.of_type.of_type.of_type == A def test_should_onetoone_reverse_convert_model(): class A(DjangoObjectType): class Meta: model = FilmDetails fields = "__all__" graphene_field = convert_django_field(Film.details.related, A._meta.registry) assert isinstance(graphene_field, graphene.Dynamic) dynamic_field = graphene_field.get_type() assert isinstance(dynamic_field, graphene.Field) assert dynamic_field.type == A @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") def test_should_postgres_array_convert_list(): field = assert_conversion( ArrayField, graphene.List, models.CharField(max_length=100) ) assert isinstance(field.type, graphene.NonNull) assert isinstance(field.type.of_type, graphene.List) assert isinstance(field.type.of_type.of_type, graphene.NonNull) assert field.type.of_type.of_type.of_type == graphene.String field = assert_conversion( ArrayField, graphene.List, models.CharField(max_length=100, null=True) ) assert isinstance(field.type, graphene.NonNull) assert isinstance(field.type.of_type, graphene.List) assert field.type.of_type.of_type == graphene.String @pytest.mark.skipif(ArrayField is MissingType, reason="ArrayField should exist") def test_should_postgres_array_multiple_convert_list(): field = assert_conversion( ArrayField, graphene.List, ArrayField(models.CharField(max_length=100)) ) assert isinstance(field.type, graphene.NonNull) assert isinstance(field.type.of_type, graphene.List) assert isinstance(field.type.of_type.of_type, graphene.List) assert isinstance(field.type.of_type.of_type.of_type, graphene.NonNull) assert field.type.of_type.of_type.of_type.of_type == graphene.String field = assert_conversion( ArrayField, graphene.List, ArrayField(models.CharField(max_length=100, null=True)), ) assert isinstance(field.type, graphene.NonNull) assert isinstance(field.type.of_type, graphene.List) assert isinstance(field.type.of_type.of_type, graphene.List) assert field.type.of_type.of_type.of_type == graphene.String @pytest.mark.skipif(HStoreField is MissingType, reason="HStoreField should exist") def test_should_postgres_hstore_convert_string(): assert_conversion(HStoreField, JSONString) @pytest.mark.skipif(RangeField is MissingType, reason="RangeField should exist") def test_should_postgres_range_convert_list(): from django.contrib.postgres.fields import IntegerRangeField field = assert_conversion(IntegerRangeField, graphene.List) assert isinstance(field.type, graphene.NonNull) assert isinstance(field.type.of_type, graphene.List) assert isinstance(field.type.of_type.of_type, graphene.NonNull) assert field.type.of_type.of_type.of_type == graphene.Int def test_generate_enum_name(): MockDjangoModelMeta = namedtuple("DjangoMeta", ["app_label", "object_name"]) # Simple case field = graphene.Field(graphene.String, name="type") model_meta = MockDjangoModelMeta(app_label="users", object_name="User") assert generate_enum_name(model_meta, field) == "UsersUserTypeChoices" # More complicated multiple work case field = graphene.Field(graphene.String, name="fizz_buzz") model_meta = MockDjangoModelMeta( app_label="some_long_app_name", object_name="SomeObject" ) assert ( generate_enum_name(model_meta, field) == "SomeLongAppNameSomeObjectFizzBuzzChoices" ) def test_generate_v2_enum_name(graphene_settings): MockDjangoModelMeta = namedtuple("DjangoMeta", ["app_label", "object_name"]) graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V2_NAMING = True # Simple case field = graphene.Field(graphene.String, name="type") model_meta = MockDjangoModelMeta(app_label="users", object_name="User") assert generate_enum_name(model_meta, field) == "UserType" # More complicated multiple work case field = graphene.Field(graphene.String, name="fizz_buzz") model_meta = MockDjangoModelMeta( app_label="some_long_app_name", object_name="SomeObject" ) assert generate_enum_name(model_meta, field) == "SomeObjectFizzBuzz" def test_choice_enum_blank_value(): """Test that choice fields with blank values work""" class ReporterType(DjangoObjectType): class Meta: model = Reporter fields = ("callable_choice",) class Query(graphene.ObjectType): reporter = graphene.Field(ReporterType) def resolve_reporter(root, info): # return a model instance with blank choice field value return Reporter(callable_choice="") schema = graphene.Schema(query=Query) result = schema.execute( """ query { reporter { callableChoice } } """ ) assert not result.errors assert result.data == { "reporter": {"callableChoice": None}, } def test_typed_choice_value(): """Test that typed choices fields are resolved correctly to the enum values""" class ReporterType(DjangoObjectType): class Meta: model = Reporter fields = ("typed_choice", "class_choice", "callable_choice") class Query(graphene.ObjectType): reporter = graphene.Field(ReporterType) def resolve_reporter(root, info): # assign choice values to the fields instead of their str or int values return Reporter( typed_choice=TypedIntChoice.CHOICE_THIS, class_choice=TypedIntChoice.CHOICE_THAT, callable_choice=TypedStrChoice.CHOICE_THIS, ) class CreateReporter(graphene.Mutation): reporter = graphene.Field(ReporterType) def mutate(root, info, **kwargs): return CreateReporter( reporter=Reporter( typed_choice=TypedIntChoice.CHOICE_THIS, class_choice=TypedIntChoice.CHOICE_THAT, callable_choice=TypedStrChoice.CHOICE_THIS, ), ) class Mutation(graphene.ObjectType): create_reporter = CreateReporter.Field() schema = graphene.Schema(query=Query, mutation=Mutation) reporter_fragment = """ fragment reporter on ReporterType { typedChoice classChoice callableChoice } """ expected_reporter = { "typedChoice": "A_1", "classChoice": "A_2", "callableChoice": "THIS", } result = schema.execute( reporter_fragment + """ query { reporter { ...reporter } } """ ) assert not result.errors assert result.data["reporter"] == expected_reporter result = schema.execute( reporter_fragment + """ mutation { createReporter { reporter { ...reporter } } } """ ) assert not result.errors assert result.data["createReporter"]["reporter"] == expected_reporter django-graphene-3.2.3/graphene_django/tests/test_fields.py000066400000000000000000000531401476451357100237240ustar00rootroot00000000000000import datetime import re import pytest from django.db.models import Count, Prefetch from graphene import List, NonNull, ObjectType, Schema, String from ..fields import DjangoListField from ..types import DjangoObjectType from .models import ( Article as ArticleModel, Film as FilmModel, FilmDetails as FilmDetailsModel, Person as PersonModel, Reporter as ReporterModel, ) class TestDjangoListField: def test_only_django_object_types(self): class Query(ObjectType): something = DjangoListField(String) with pytest.raises(TypeError) as excinfo: Schema(query=Query) assert ( "Query fields cannot be resolved. DjangoListField only accepts DjangoObjectType types as underlying type" in str(excinfo.value) ) def test_only_import_paths(self): list_field = DjangoListField("graphene_django.tests.schema.Human") from .schema import Human assert list_field._type.of_type.of_type is Human def test_non_null_type(self): class Reporter(DjangoObjectType): class Meta: model = ReporterModel fields = ("first_name",) list_field = DjangoListField(NonNull(Reporter)) assert isinstance(list_field.type, List) assert isinstance(list_field.type.of_type, NonNull) assert list_field.type.of_type.of_type is Reporter def test_get_django_model(self): class Reporter(DjangoObjectType): class Meta: model = ReporterModel fields = ("first_name",) list_field = DjangoListField(Reporter) assert list_field.model is ReporterModel def test_list_field_default_queryset(self): class Reporter(DjangoObjectType): class Meta: model = ReporterModel fields = ("first_name",) class Query(ObjectType): reporters = DjangoListField(Reporter) schema = Schema(query=Query) query = """ query { reporters { firstName } } """ ReporterModel.objects.create(first_name="Tara", last_name="West") ReporterModel.objects.create(first_name="Debra", last_name="Payne") result = schema.execute(query) assert not result.errors assert result.data == { "reporters": [{"firstName": "Tara"}, {"firstName": "Debra"}] } def test_list_field_queryset_is_not_cached(self): class Reporter(DjangoObjectType): class Meta: model = ReporterModel fields = ("first_name",) class Query(ObjectType): reporters = DjangoListField(Reporter) schema = Schema(query=Query) query = """ query { reporters { firstName } } """ result = schema.execute(query) assert not result.errors assert result.data == {"reporters": []} ReporterModel.objects.create(first_name="Tara", last_name="West") ReporterModel.objects.create(first_name="Debra", last_name="Payne") result = schema.execute(query) assert not result.errors assert result.data == { "reporters": [{"firstName": "Tara"}, {"firstName": "Debra"}] } def test_override_resolver(self): class Reporter(DjangoObjectType): class Meta: model = ReporterModel fields = ("first_name",) class Query(ObjectType): reporters = DjangoListField(Reporter) def resolve_reporters(_, info): return ReporterModel.objects.filter(first_name="Tara") schema = Schema(query=Query) query = """ query { reporters { firstName } } """ ReporterModel.objects.create(first_name="Tara", last_name="West") ReporterModel.objects.create(first_name="Debra", last_name="Payne") result = schema.execute(query) assert not result.errors assert result.data == {"reporters": [{"firstName": "Tara"}]} def test_nested_list_field(self): class Article(DjangoObjectType): class Meta: model = ArticleModel fields = ("headline",) class Reporter(DjangoObjectType): class Meta: model = ReporterModel fields = ("first_name", "articles") class Query(ObjectType): reporters = DjangoListField(Reporter) schema = Schema(query=Query) query = """ query { reporters { firstName articles { headline } } } """ r1 = ReporterModel.objects.create(first_name="Tara", last_name="West") ReporterModel.objects.create(first_name="Debra", last_name="Payne") ArticleModel.objects.create( headline="Amazing news", reporter=r1, pub_date=datetime.date.today(), pub_date_time=datetime.datetime.now(), editor=r1, ) ArticleModel.objects.create( headline="Not so good news", reporter=r1, pub_date=datetime.date.today(), pub_date_time=datetime.datetime.now(), editor=r1, ) result = schema.execute(query) assert not result.errors assert result.data == { "reporters": [ { "firstName": "Tara", "articles": [ {"headline": "Amazing news"}, {"headline": "Not so good news"}, ], }, {"firstName": "Debra", "articles": []}, ] } def test_override_resolver_nested_list_field(self): class Article(DjangoObjectType): class Meta: model = ArticleModel fields = ("headline",) class Reporter(DjangoObjectType): class Meta: model = ReporterModel fields = ("first_name", "articles") def resolve_articles(reporter, info): return reporter.articles.filter(headline__contains="Amazing") class Query(ObjectType): reporters = DjangoListField(Reporter) schema = Schema(query=Query) query = """ query { reporters { firstName articles { headline } } } """ r1 = ReporterModel.objects.create(first_name="Tara", last_name="West") ReporterModel.objects.create(first_name="Debra", last_name="Payne") ArticleModel.objects.create( headline="Amazing news", reporter=r1, pub_date=datetime.date.today(), pub_date_time=datetime.datetime.now(), editor=r1, ) ArticleModel.objects.create( headline="Not so good news", reporter=r1, pub_date=datetime.date.today(), pub_date_time=datetime.datetime.now(), editor=r1, ) result = schema.execute(query) assert not result.errors assert result.data == { "reporters": [ {"firstName": "Tara", "articles": [{"headline": "Amazing news"}]}, {"firstName": "Debra", "articles": []}, ] } def test_same_type_nested_list_field(self): class Person(DjangoObjectType): class Meta: model = PersonModel fields = ("name", "parent") children = DjangoListField(lambda: Person) class Query(ObjectType): persons = DjangoListField(Person) schema = Schema(query=Query) query = """ query { persons { name children { name } } } """ p1 = PersonModel.objects.create(name="Tara") PersonModel.objects.create(name="Debra") PersonModel.objects.create( name="Toto", parent=p1, ) PersonModel.objects.create( name="Tata", parent=p1, ) result = schema.execute(query) assert not result.errors assert result.data == { "persons": [ { "name": "Tara", "children": [ {"name": "Toto"}, {"name": "Tata"}, ], }, { "name": "Debra", "children": [], }, { "name": "Toto", "children": [], }, { "name": "Tata", "children": [], }, ] } def test_get_queryset_filter(self): class Reporter(DjangoObjectType): class Meta: model = ReporterModel fields = ("first_name", "articles") @classmethod def get_queryset(cls, queryset, info): # Only get reporters with at least 1 article return queryset.annotate(article_count=Count("articles")).filter( article_count__gt=0 ) class Query(ObjectType): reporters = DjangoListField(Reporter) def resolve_reporters(_, info): return ReporterModel.objects.all() schema = Schema(query=Query) query = """ query { reporters { firstName } } """ r1 = ReporterModel.objects.create(first_name="Tara", last_name="West") ReporterModel.objects.create(first_name="Debra", last_name="Payne") ArticleModel.objects.create( headline="Amazing news", reporter=r1, pub_date=datetime.date.today(), pub_date_time=datetime.datetime.now(), editor=r1, ) result = schema.execute(query) assert not result.errors assert result.data == {"reporters": [{"firstName": "Tara"}]} def test_resolve_list(self): """Resolving a plain list should work (and not call get_queryset)""" class Reporter(DjangoObjectType): class Meta: model = ReporterModel fields = ("first_name", "articles") @classmethod def get_queryset(cls, queryset, info): # Only get reporters with at least 1 article return queryset.annotate(article_count=Count("articles")).filter( article_count__gt=0 ) class Query(ObjectType): reporters = DjangoListField(Reporter) def resolve_reporters(_, info): return [ReporterModel.objects.get(first_name="Debra")] schema = Schema(query=Query) query = """ query { reporters { firstName } } """ r1 = ReporterModel.objects.create(first_name="Tara", last_name="West") ReporterModel.objects.create(first_name="Debra", last_name="Payne") ArticleModel.objects.create( headline="Amazing news", reporter=r1, pub_date=datetime.date.today(), pub_date_time=datetime.datetime.now(), editor=r1, ) result = schema.execute(query) assert not result.errors assert result.data == {"reporters": [{"firstName": "Debra"}]} def test_get_queryset_foreign_key(self): class Article(DjangoObjectType): class Meta: model = ArticleModel fields = ("headline",) @classmethod def get_queryset(cls, queryset, info): # Rose tinted glasses return queryset.exclude(headline__contains="Not so good") class Reporter(DjangoObjectType): class Meta: model = ReporterModel fields = ("first_name", "articles") class Query(ObjectType): reporters = DjangoListField(Reporter) schema = Schema(query=Query) query = """ query { reporters { firstName articles { headline } } } """ r1 = ReporterModel.objects.create(first_name="Tara", last_name="West") ReporterModel.objects.create(first_name="Debra", last_name="Payne") ArticleModel.objects.create( headline="Amazing news", reporter=r1, pub_date=datetime.date.today(), pub_date_time=datetime.datetime.now(), editor=r1, ) ArticleModel.objects.create( headline="Not so good news", reporter=r1, pub_date=datetime.date.today(), pub_date_time=datetime.datetime.now(), editor=r1, ) result = schema.execute(query) assert not result.errors assert result.data == { "reporters": [ {"firstName": "Tara", "articles": [{"headline": "Amazing news"}]}, {"firstName": "Debra", "articles": []}, ] } def test_resolve_list_external_resolver(self): """Resolving a plain list from external resolver should work (and not call get_queryset)""" class Reporter(DjangoObjectType): class Meta: model = ReporterModel fields = ("first_name", "articles") @classmethod def get_queryset(cls, queryset, info): # Only get reporters with at least 1 article return queryset.annotate(article_count=Count("articles")).filter( article_count__gt=0 ) def resolve_reporters(_, info): return [ReporterModel.objects.get(first_name="Debra")] class Query(ObjectType): reporters = DjangoListField(Reporter, resolver=resolve_reporters) schema = Schema(query=Query) query = """ query { reporters { firstName } } """ r1 = ReporterModel.objects.create(first_name="Tara", last_name="West") ReporterModel.objects.create(first_name="Debra", last_name="Payne") ArticleModel.objects.create( headline="Amazing news", reporter=r1, pub_date=datetime.date.today(), pub_date_time=datetime.datetime.now(), editor=r1, ) result = schema.execute(query) assert not result.errors assert result.data == {"reporters": [{"firstName": "Debra"}]} def test_get_queryset_filter_external_resolver(self): class Reporter(DjangoObjectType): class Meta: model = ReporterModel fields = ("first_name", "articles") @classmethod def get_queryset(cls, queryset, info): # Only get reporters with at least 1 article return queryset.annotate(article_count=Count("articles")).filter( article_count__gt=0 ) def resolve_reporters(_, info): return ReporterModel.objects.all() class Query(ObjectType): reporters = DjangoListField(Reporter, resolver=resolve_reporters) schema = Schema(query=Query) query = """ query { reporters { firstName } } """ r1 = ReporterModel.objects.create(first_name="Tara", last_name="West") ReporterModel.objects.create(first_name="Debra", last_name="Payne") ArticleModel.objects.create( headline="Amazing news", reporter=r1, pub_date=datetime.date.today(), pub_date_time=datetime.datetime.now(), editor=r1, ) result = schema.execute(query) assert not result.errors assert result.data == {"reporters": [{"firstName": "Tara"}]} def test_select_related_and_prefetch_related_are_respected( self, django_assert_num_queries ): class Article(DjangoObjectType): class Meta: model = ArticleModel fields = ("headline", "editor", "reporter") class Film(DjangoObjectType): class Meta: model = FilmModel fields = ("genre", "details") class FilmDetail(DjangoObjectType): class Meta: model = FilmDetailsModel fields = ("location",) class Reporter(DjangoObjectType): class Meta: model = ReporterModel fields = ("first_name", "articles", "films") class Query(ObjectType): articles = DjangoListField(Article) @staticmethod def resolve_articles(root, info): # Optimize for querying associated editors and reporters, and the films and film # details of those reporters. This is similar to what would happen using a library # like https://github.com/tfoxy/graphene-django-optimizer for a query like the one # below (albeit simplified and hardcoded here). return ArticleModel.objects.select_related( "editor", "reporter" ).prefetch_related( Prefetch( "reporter__films", queryset=FilmModel.objects.select_related("details"), ), ) schema = Schema(query=Query) query = """ query { articles { headline editor { firstName } reporter { firstName films { genre details { location } } } } } """ r1 = ReporterModel.objects.create(first_name="Tara", last_name="West") r2 = ReporterModel.objects.create(first_name="Debra", last_name="Payne") ArticleModel.objects.create( headline="Amazing news", reporter=r1, pub_date=datetime.date.today(), pub_date_time=datetime.datetime.now(), editor=r2, ) ArticleModel.objects.create( headline="Not so good news", reporter=r2, pub_date=datetime.date.today(), pub_date_time=datetime.datetime.now(), editor=r1, ) film1 = FilmModel.objects.create(genre="ac") film2 = FilmModel.objects.create(genre="ot") film3 = FilmModel.objects.create(genre="do") FilmDetailsModel.objects.create(location="Hollywood", film=film1) FilmDetailsModel.objects.create(location="Antarctica", film=film3) r1.films.add(film1, film2) r2.films.add(film3) # We expect 2 queries to be performed based on the above resolver definition: one for all # articles joined with the reporters model (for associated editors and reporters), and one # for the films prefetch (which includes its `select_related` JOIN logic in its queryset) with django_assert_num_queries(2) as captured: result = schema.execute(query) assert not result.errors assert result.data == { "articles": [ { "headline": "Amazing news", "editor": {"firstName": "Debra"}, "reporter": { "firstName": "Tara", "films": [ {"genre": "AC", "details": {"location": "Hollywood"}}, {"genre": "OT", "details": None}, ], }, }, { "headline": "Not so good news", "editor": {"firstName": "Tara"}, "reporter": { "firstName": "Debra", "films": [ {"genre": "DO", "details": {"location": "Antarctica"}}, ], }, }, ] } assert len(captured.captured_queries) == 2 # Sanity-check # First we should have queried for all articles in a single query, joining on the reporters # model (for the editors and reporters ForeignKeys) assert re.match( r'SELECT .* "tests_article" INNER JOIN "tests_reporter"', captured.captured_queries[0]["sql"], ) # Then we should have queried for all of the films of all reporters, joined with the film # details for each film, using a single query assert re.match( r'SELECT .* FROM "tests_film" INNER JOIN "tests_film_reporters" .* LEFT OUTER JOIN "tests_filmdetails"', captured.captured_queries[1]["sql"], ) django-graphene-3.2.3/graphene_django/tests/test_forms.py000066400000000000000000000016671476451357100236130ustar00rootroot00000000000000from django.core.exceptions import ValidationError from pytest import raises from ..forms import GlobalIDFormField, GlobalIDMultipleChoiceField # 'TXlUeXBlOmFiYw==' -> 'MyType', 'abc' def test_global_id_valid(): field = GlobalIDFormField() field.clean("TXlUeXBlOmFiYw==") def test_global_id_invalid(): field = GlobalIDFormField() with raises(ValidationError): field.clean("badvalue") def test_global_id_multiple_valid(): field = GlobalIDMultipleChoiceField() field.clean(["TXlUeXBlOmFiYw==", "TXlUeXBlOmFiYw=="]) def test_global_id_multiple_invalid(): field = GlobalIDMultipleChoiceField() with raises(ValidationError): field.clean(["badvalue", "another bad avue"]) def test_global_id_none(): field = GlobalIDFormField() with raises(ValidationError): field.clean(None) def test_global_id_none_optional(): field = GlobalIDFormField(required=False) field.clean(None) django-graphene-3.2.3/graphene_django/tests/test_get_queryset.py000066400000000000000000000444371476451357100252070ustar00rootroot00000000000000import pytest from graphql_relay import to_global_id import graphene from graphene.relay import Node from ..types import DjangoObjectType from .models import Article, Film, FilmDetails, Reporter class TestShouldCallGetQuerySetOnForeignKey: """ Check that the get_queryset method is called in both forward and reversed direction of a foreignkey on types. (see issue #1111) NOTE: For now, we do not expect this get_queryset method to be called for nested objects, as the original attempt to do so prevented SQL query-optimization with `select_related`/`prefetch_related` and caused N+1 queries. See discussions here https://github.com/graphql-python/graphene-django/pull/1315/files#r1015659857 and here https://github.com/graphql-python/graphene-django/pull/1401. """ @pytest.fixture(autouse=True) def setup_schema(self): class ReporterType(DjangoObjectType): class Meta: model = Reporter fields = "__all__" @classmethod def get_queryset(cls, queryset, info): if info.context and info.context.get("admin"): return queryset raise Exception("Not authorized to access reporters.") class ArticleType(DjangoObjectType): class Meta: model = Article fields = "__all__" @classmethod def get_queryset(cls, queryset, info): return queryset.exclude(headline__startswith="Draft") class Query(graphene.ObjectType): reporter = graphene.Field(ReporterType, id=graphene.ID(required=True)) article = graphene.Field(ArticleType, id=graphene.ID(required=True)) def resolve_reporter(self, info, id): return ( ReporterType.get_queryset(Reporter.objects, info) .filter(id=id) .last() ) def resolve_article(self, info, id): return ( ArticleType.get_queryset(Article.objects, info).filter(id=id).last() ) self.schema = graphene.Schema(query=Query) self.reporter = Reporter.objects.create(first_name="Jane", last_name="Doe") self.articles = [ Article.objects.create( headline="A fantastic article", reporter=self.reporter, editor=self.reporter, ), Article.objects.create( headline="Draft: My next best seller", reporter=self.reporter, editor=self.reporter, ), ] def test_get_queryset_called_on_field(self): # If a user tries to access an article it is fine as long as it's not a draft one query = """ query getArticle($id: ID!) { article(id: $id) { headline } } """ # Non-draft result = self.schema.execute(query, variables={"id": self.articles[0].id}) assert not result.errors assert result.data["article"] == { "headline": "A fantastic article", } # Draft result = self.schema.execute(query, variables={"id": self.articles[1].id}) assert not result.errors assert result.data["article"] is None # If a non admin user tries to access a reporter they should get our authorization error query = """ query getReporter($id: ID!) { reporter(id: $id) { firstName } } """ result = self.schema.execute(query, variables={"id": self.reporter.id}) assert len(result.errors) == 1 assert result.errors[0].message == "Not authorized to access reporters." # An admin user should be able to get reporters query = """ query getReporter($id: ID!) { reporter(id: $id) { firstName } } """ result = self.schema.execute( query, variables={"id": self.reporter.id}, context_value={"admin": True}, ) assert not result.errors assert result.data == {"reporter": {"firstName": "Jane"}} def test_get_queryset_called_on_foreignkey(self): # If a user tries to access a reporter through an article they should get our authorization error query = """ query getArticle($id: ID!) { article(id: $id) { headline reporter { firstName } } } """ result = self.schema.execute(query, variables={"id": self.articles[0].id}) assert len(result.errors) == 1 assert result.errors[0].message == "Not authorized to access reporters." # An admin user should be able to get reporters through an article query = """ query getArticle($id: ID!) { article(id: $id) { headline reporter { firstName } } } """ result = self.schema.execute( query, variables={"id": self.articles[0].id}, context_value={"admin": True}, ) assert not result.errors assert result.data["article"] == { "headline": "A fantastic article", "reporter": {"firstName": "Jane"}, } # An admin user should not be able to access draft article through a reporter query = """ query getReporter($id: ID!) { reporter(id: $id) { firstName articles { headline } } } """ result = self.schema.execute( query, variables={"id": self.reporter.id}, context_value={"admin": True}, ) assert not result.errors assert result.data["reporter"] == { "firstName": "Jane", "articles": [{"headline": "A fantastic article"}], } class TestShouldCallGetQuerySetOnForeignKeyNode: """ Check that the get_queryset method is called in both forward and reversed direction of a foreignkey on types using a node interface. (see issue #1111) """ @pytest.fixture(autouse=True) def setup_schema(self): class ReporterType(DjangoObjectType): class Meta: model = Reporter fields = "__all__" interfaces = (Node,) @classmethod def get_queryset(cls, queryset, info): if info.context and info.context.get("admin"): return queryset raise Exception("Not authorized to access reporters.") class ArticleType(DjangoObjectType): class Meta: model = Article fields = "__all__" interfaces = (Node,) @classmethod def get_queryset(cls, queryset, info): return queryset.exclude(headline__startswith="Draft") class Query(graphene.ObjectType): reporter = Node.Field(ReporterType) article = Node.Field(ArticleType) self.schema = graphene.Schema(query=Query) self.reporter = Reporter.objects.create(first_name="Jane", last_name="Doe") self.articles = [ Article.objects.create( headline="A fantastic article", reporter=self.reporter, editor=self.reporter, ), Article.objects.create( headline="Draft: My next best seller", reporter=self.reporter, editor=self.reporter, ), ] def test_get_queryset_called_on_node(self): # If a user tries to access an article it is fine as long as it's not a draft one query = """ query getArticle($id: ID!) { article(id: $id) { headline } } """ # Non-draft result = self.schema.execute( query, variables={"id": to_global_id("ArticleType", self.articles[0].id)} ) assert not result.errors assert result.data["article"] == { "headline": "A fantastic article", } # Draft result = self.schema.execute( query, variables={"id": to_global_id("ArticleType", self.articles[1].id)} ) assert not result.errors assert result.data["article"] is None # If a non admin user tries to access a reporter they should get our authorization error query = """ query getReporter($id: ID!) { reporter(id: $id) { firstName } } """ result = self.schema.execute( query, variables={"id": to_global_id("ReporterType", self.reporter.id)} ) assert len(result.errors) == 1 assert result.errors[0].message == "Not authorized to access reporters." # An admin user should be able to get reporters query = """ query getReporter($id: ID!) { reporter(id: $id) { firstName } } """ result = self.schema.execute( query, variables={"id": to_global_id("ReporterType", self.reporter.id)}, context_value={"admin": True}, ) assert not result.errors assert result.data == {"reporter": {"firstName": "Jane"}} def test_get_queryset_called_on_foreignkey(self): # If a user tries to access a reporter through an article they should get our authorization error query = """ query getArticle($id: ID!) { article(id: $id) { headline reporter { firstName } } } """ result = self.schema.execute( query, variables={"id": to_global_id("ArticleType", self.articles[0].id)} ) assert len(result.errors) == 1 assert result.errors[0].message == "Not authorized to access reporters." # An admin user should be able to get reporters through an article query = """ query getArticle($id: ID!) { article(id: $id) { headline reporter { firstName } } } """ result = self.schema.execute( query, variables={"id": to_global_id("ArticleType", self.articles[0].id)}, context_value={"admin": True}, ) assert not result.errors assert result.data["article"] == { "headline": "A fantastic article", "reporter": {"firstName": "Jane"}, } # An admin user should not be able to access draft article through a reporter query = """ query getReporter($id: ID!) { reporter(id: $id) { firstName articles { edges { node { headline } } } } } """ result = self.schema.execute( query, variables={"id": to_global_id("ReporterType", self.reporter.id)}, context_value={"admin": True}, ) assert not result.errors assert result.data["reporter"] == { "firstName": "Jane", "articles": {"edges": [{"node": {"headline": "A fantastic article"}}]}, } class TestShouldCallGetQuerySetOnOneToOne: @pytest.fixture(autouse=True) def setup_schema(self): class FilmDetailsType(DjangoObjectType): class Meta: model = FilmDetails fields = "__all__" @classmethod def get_queryset(cls, queryset, info): if info.context and info.context.get("permission_get_film_details"): return queryset raise Exception("Not authorized to access film details.") class FilmType(DjangoObjectType): class Meta: model = Film fields = "__all__" @classmethod def get_queryset(cls, queryset, info): if info.context and info.context.get("permission_get_film"): return queryset raise Exception("Not authorized to access film.") class Query(graphene.ObjectType): film_details = graphene.Field( FilmDetailsType, id=graphene.ID(required=True) ) film = graphene.Field(FilmType, id=graphene.ID(required=True)) def resolve_film_details(self, info, id): return ( FilmDetailsType.get_queryset(FilmDetails.objects, info) .filter(id=id) .last() ) def resolve_film(self, info, id): return FilmType.get_queryset(Film.objects, info).filter(id=id).last() self.schema = graphene.Schema(query=Query) self.films = [ Film.objects.create( genre="do", ), Film.objects.create( genre="ac", ), ] self.film_details = [ FilmDetails.objects.create( film=self.films[0], ), FilmDetails.objects.create( film=self.films[1], ), ] def test_get_queryset_called_on_field(self): # A user tries to access a film query = """ query getFilm($id: ID!) { film(id: $id) { genre } } """ # With `permission_get_film` result = self.schema.execute( query, variables={"id": self.films[0].id}, context_value={"permission_get_film": True}, ) assert not result.errors assert result.data["film"] == { "genre": "DO", } # Without `permission_get_film` result = self.schema.execute( query, variables={"id": self.films[1].id}, context_value={"permission_get_film": False}, ) assert len(result.errors) == 1 assert result.errors[0].message == "Not authorized to access film." # A user tries to access a film details query = """ query getFilmDetails($id: ID!) { filmDetails(id: $id) { location } } """ # With `permission_get_film` result = self.schema.execute( query, variables={"id": self.film_details[0].id}, context_value={"permission_get_film_details": True}, ) assert not result.errors assert result.data == {"filmDetails": {"location": ""}} # Without `permission_get_film` result = self.schema.execute( query, variables={"id": self.film_details[0].id}, context_value={"permission_get_film_details": False}, ) assert len(result.errors) == 1 assert result.errors[0].message == "Not authorized to access film details." def test_get_queryset_called_on_foreignkey(self, django_assert_num_queries): # A user tries to access a film details through a film query = """ query getFilm($id: ID!) { film(id: $id) { genre details { location } } } """ # With `permission_get_film_details` with django_assert_num_queries(2): result = self.schema.execute( query, variables={"id": self.films[0].id}, context_value={ "permission_get_film": True, "permission_get_film_details": True, }, ) assert not result.errors assert result.data["film"] == { "genre": "DO", "details": {"location": ""}, } # Without `permission_get_film_details` with django_assert_num_queries(1): result = self.schema.execute( query, variables={"id": self.films[0].id}, context_value={ "permission_get_film": True, "permission_get_film_details": False, }, ) assert len(result.errors) == 1 assert result.errors[0].message == "Not authorized to access film details." # A user tries to access a film through a film details query = """ query getFilmDetails($id: ID!) { filmDetails(id: $id) { location film { genre } } } """ # With `permission_get_film` with django_assert_num_queries(2): result = self.schema.execute( query, variables={"id": self.film_details[0].id}, context_value={ "permission_get_film": True, "permission_get_film_details": True, }, ) assert not result.errors assert result.data["filmDetails"] == { "location": "", "film": {"genre": "DO"}, } # Without `permission_get_film` with django_assert_num_queries(1): result = self.schema.execute( query, variables={"id": self.film_details[1].id}, context_value={ "permission_get_film": False, "permission_get_film_details": True, }, ) assert len(result.errors) == 1 assert result.errors[0].message == "Not authorized to access film." django-graphene-3.2.3/graphene_django/tests/test_query.py000066400000000000000000001677661476451357100236470ustar00rootroot00000000000000import base64 import datetime from unittest.mock import ANY, Mock import pytest from django.db import models from django.db.models import Q from django.utils.functional import SimpleLazyObject from graphql_relay import to_global_id from pytest import raises import graphene from graphene.relay import Node from ..compat import IntegerRangeField, MissingType from ..fields import DjangoConnectionField from ..types import DjangoObjectType from ..utils import DJANGO_FILTER_INSTALLED from .models import ( APNewsReporter, Article, CNNReporter, Film, FilmDetails, Person, Pet, Reporter, ) def test_should_query_only_fields(): with raises(Exception): class ReporterType(DjangoObjectType): class Meta: model = Reporter fields = ("articles",) schema = graphene.Schema(query=ReporterType) query = """ query ReporterQuery { articles } """ result = schema.execute(query) assert not result.errors def test_should_query_simplelazy_objects(): class ReporterType(DjangoObjectType): class Meta: model = Reporter fields = ("id",) class Query(graphene.ObjectType): reporter = graphene.Field(ReporterType) def resolve_reporter(self, info): return SimpleLazyObject(lambda: Reporter(id=1)) schema = graphene.Schema(query=Query) query = """ query { reporter { id } } """ result = schema.execute(query) assert not result.errors assert result.data == {"reporter": {"id": "1"}} def test_should_query_wrapped_simplelazy_objects(): class ReporterType(DjangoObjectType): class Meta: model = Reporter fields = ("id",) class Query(graphene.ObjectType): reporter = graphene.Field(ReporterType) def resolve_reporter(self, info): return SimpleLazyObject(lambda: SimpleLazyObject(lambda: Reporter(id=1))) schema = graphene.Schema(query=Query) query = """ query { reporter { id } } """ result = schema.execute(query) assert not result.errors assert result.data == {"reporter": {"id": "1"}} def test_should_query_well(): class ReporterType(DjangoObjectType): class Meta: model = Reporter fields = "__all__" class Query(graphene.ObjectType): reporter = graphene.Field(ReporterType) def resolve_reporter(self, info): return Reporter(first_name="ABA", last_name="X") query = """ query ReporterQuery { reporter { firstName, lastName, email } } """ expected = {"reporter": {"firstName": "ABA", "lastName": "X", "email": ""}} schema = graphene.Schema(query=Query) result = schema.execute(query) assert not result.errors assert result.data == expected @pytest.mark.skipif(IntegerRangeField is MissingType, reason="RangeField should exist") def test_should_query_postgres_fields(): from django.contrib.postgres.fields import ( ArrayField, HStoreField, IntegerRangeField, ) class Event(models.Model): ages = IntegerRangeField(help_text="The age ranges") data = models.JSONField(help_text="Data") store = HStoreField() tags = ArrayField(models.CharField(max_length=50)) class EventType(DjangoObjectType): class Meta: model = Event fields = "__all__" class Query(graphene.ObjectType): event = graphene.Field(EventType) def resolve_event(self, info): return Event( ages=(0, 10), data={"angry_babies": True}, store={"h": "store"}, tags=["child", "angry", "babies"], ) schema = graphene.Schema(query=Query) query = """ query myQuery { event { ages tags data store } } """ expected = { "event": { "ages": [0, 10], "tags": ["child", "angry", "babies"], "data": '{"angry_babies": true}', "store": '{"h": "store"}', } } result = schema.execute(query) assert not result.errors assert result.data == expected def test_should_node(): class ReporterNode(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" @classmethod def get_node(cls, info, id): return Reporter(id=2, first_name="Cookie Monster") def resolve_articles(self, info, **args): return [Article(headline="Hi!")] class ArticleNode(DjangoObjectType): class Meta: model = Article interfaces = (Node,) fields = "__all__" @classmethod def get_node(cls, info, id): return Article( id=1, headline="Article node", pub_date=datetime.date(2002, 3, 11) ) class Query(graphene.ObjectType): node = Node.Field() reporter = graphene.Field(ReporterNode) article = graphene.Field(ArticleNode) def resolve_reporter(self, info): return Reporter(id=1, first_name="ABA", last_name="X") query = """ query ReporterQuery { reporter { id, firstName, articles { edges { node { headline } } } lastName, email } myArticle: node(id:"QXJ0aWNsZU5vZGU6MQ==") { id ... on ReporterNode { firstName } ... on ArticleNode { headline pubDate } } } """ expected = { "reporter": { "id": "UmVwb3J0ZXJOb2RlOjE=", "firstName": "ABA", "lastName": "X", "email": "", "articles": {"edges": [{"node": {"headline": "Hi!"}}]}, }, "myArticle": { "id": "QXJ0aWNsZU5vZGU6MQ==", "headline": "Article node", "pubDate": "2002-03-11", }, } schema = graphene.Schema(query=Query) result = schema.execute(query) assert not result.errors assert result.data == expected def test_should_query_onetoone_fields(): film = Film.objects.create(id=1) film_details = FilmDetails.objects.create(id=1, film=film) class FilmNode(DjangoObjectType): class Meta: model = Film interfaces = (Node,) fields = "__all__" class FilmDetailsNode(DjangoObjectType): class Meta: model = FilmDetails interfaces = (Node,) fields = "__all__" class Query(graphene.ObjectType): film = graphene.Field(FilmNode) film_details = graphene.Field(FilmDetailsNode) def resolve_film(root, info): return film def resolve_film_details(root, info): return film_details query = """ query FilmQuery { filmDetails { id film { id } } film { id details { id } } } """ expected = { "filmDetails": { "id": "RmlsbURldGFpbHNOb2RlOjE=", "film": {"id": "RmlsbU5vZGU6MQ=="}, }, "film": { "id": "RmlsbU5vZGU6MQ==", "details": {"id": "RmlsbURldGFpbHNOb2RlOjE="}, }, } schema = graphene.Schema(query=Query) result = schema.execute(query) assert not result.errors assert result.data == expected def test_should_query_connectionfields(): class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = ("articles",) class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) def resolve_all_reporters(self, info, **args): return [Reporter(id=1)] schema = graphene.Schema(query=Query) query = """ query ReporterConnectionQuery { allReporters { pageInfo { hasNextPage } edges { node { id } } } } """ result = schema.execute(query) assert not result.errors assert result.data == { "allReporters": { "pageInfo": {"hasNextPage": False}, "edges": [{"node": {"id": "UmVwb3J0ZXJUeXBlOjE="}}], } } def test_should_keep_annotations(): from django.db.models import Avg, Count class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = ("articles",) class ArticleType(DjangoObjectType): class Meta: model = Article interfaces = (Node,) fields = "__all__" filter_fields = ("lang",) class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) all_articles = DjangoConnectionField(ArticleType) def resolve_all_reporters(self, info, **args): return Reporter.objects.annotate(articles_c=Count("articles")).order_by( "articles_c" ) def resolve_all_articles(self, info, **args): return Article.objects.annotate(import_avg=Avg("importance")).order_by( "import_avg" ) schema = graphene.Schema(query=Query) query = """ query ReporterConnectionQuery { allReporters { pageInfo { hasNextPage } edges { node { id } } } allArticles { pageInfo { hasNextPage } edges { node { id } } } } """ result = schema.execute(query) assert not result.errors @pytest.mark.skipif( not DJANGO_FILTER_INSTALLED, reason="django-filter should be installed" ) def test_should_query_node_filtering(): class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" class ArticleType(DjangoObjectType): class Meta: model = Article interfaces = (Node,) fields = "__all__" filter_fields = ("lang",) convert_choices_to_enum = False class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) r = Reporter.objects.create( first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 ) Article.objects.create( headline="Article Node 1", pub_date=datetime.date.today(), pub_date_time=datetime.datetime.now(), reporter=r, editor=r, lang="es", ) Article.objects.create( headline="Article Node 2", pub_date=datetime.date.today(), pub_date_time=datetime.datetime.now(), reporter=r, editor=r, lang="en", ) schema = graphene.Schema(query=Query) query = """ query NodeFilteringQuery { allReporters { edges { node { id articles(lang: "es") { edges { node { id } } } } } } } """ expected = { "allReporters": { "edges": [ { "node": { "id": "UmVwb3J0ZXJUeXBlOjE=", "articles": { "edges": [{"node": {"id": "QXJ0aWNsZVR5cGU6MQ=="}}] }, } } ] } } result = schema.execute(query) assert not result.errors assert result.data == expected @pytest.mark.skipif( not DJANGO_FILTER_INSTALLED, reason="django-filter should be installed" ) def test_should_query_node_filtering_with_distinct_queryset(): class FilmType(DjangoObjectType): class Meta: model = Film interfaces = (Node,) fields = "__all__" filter_fields = ("genre",) class Query(graphene.ObjectType): films = DjangoConnectionField(FilmType) # def resolve_all_reporters_with_berlin_films(self, args, context, info): # return Reporter.objects.filter(Q(films__film__location__contains="Berlin") | Q(a_choice=1)) def resolve_films(self, info, **args): return Film.objects.filter( Q(details__location__contains="Berlin") | Q(genre__in=["ot"]) ).distinct() f = Film.objects.create() FilmDetails.objects.create(location="Berlin", film=f) schema = graphene.Schema(query=Query) query = """ query NodeFilteringQuery { films { edges { node { genre } } } } """ expected = {"films": {"edges": [{"node": {"genre": "OT"}}]}} result = schema.execute(query) assert not result.errors assert result.data == expected @pytest.mark.skipif( not DJANGO_FILTER_INSTALLED, reason="django-filter should be installed" ) def test_should_query_node_multiple_filtering(): class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" class ArticleType(DjangoObjectType): class Meta: model = Article interfaces = (Node,) fields = "__all__" filter_fields = ("lang", "headline") convert_choices_to_enum = False class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) r = Reporter.objects.create( first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 ) Article.objects.create( headline="Article Node 1", pub_date=datetime.date.today(), pub_date_time=datetime.datetime.now(), reporter=r, editor=r, lang="es", ) Article.objects.create( headline="Article Node 2", pub_date=datetime.date.today(), pub_date_time=datetime.datetime.now(), reporter=r, editor=r, lang="es", ) Article.objects.create( headline="Article Node 3", pub_date=datetime.date.today(), pub_date_time=datetime.datetime.now(), reporter=r, editor=r, lang="en", ) schema = graphene.Schema(query=Query) query = """ query NodeFilteringQuery { allReporters { edges { node { id articles(lang: "es", headline: "Article Node 1") { edges { node { id } } } } } } } """ expected = { "allReporters": { "edges": [ { "node": { "id": "UmVwb3J0ZXJUeXBlOjE=", "articles": { "edges": [{"node": {"id": "QXJ0aWNsZVR5cGU6MQ=="}}] }, } } ] } } result = schema.execute(query) assert not result.errors assert result.data == expected def test_should_enforce_first_or_last(graphene_settings): graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST = True class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) Reporter.objects.create( first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 ) schema = graphene.Schema(query=Query) query = """ query NodeFilteringQuery { allReporters { edges { node { id } } } } """ expected = {"allReporters": None} result = schema.execute(query) assert len(result.errors) == 1 assert str(result.errors[0]).startswith( "You must provide a `first` or `last` value to properly " "paginate the `allReporters` connection.\n" ) assert result.data == expected def test_should_error_if_first_is_greater_than_max(graphene_settings): graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 100 class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) assert Query.all_reporters.max_limit == 100 Reporter.objects.create( first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 ) schema = graphene.Schema(query=Query) query = """ query NodeFilteringQuery { allReporters(first: 101) { edges { node { id } } } } """ expected = {"allReporters": None} result = schema.execute(query) assert len(result.errors) == 1 assert str(result.errors[0]).startswith( "Requesting 101 records on the `allReporters` connection " "exceeds the `first` limit of 100 records.\n" ) assert result.data == expected def test_should_error_if_last_is_greater_than_max(graphene_settings): graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 100 class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) assert Query.all_reporters.max_limit == 100 Reporter.objects.create( first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 ) schema = graphene.Schema(query=Query) query = """ query NodeFilteringQuery { allReporters(last: 101) { edges { node { id } } } } """ expected = {"allReporters": None} result = schema.execute(query) assert len(result.errors) == 1 assert str(result.errors[0]).startswith( "Requesting 101 records on the `allReporters` connection " "exceeds the `last` limit of 100 records.\n" ) assert result.data == expected def test_should_query_promise_connectionfields(): from promise import Promise class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) def resolve_all_reporters(self, info, **args): return Promise.resolve([Reporter(id=1)]).get() schema = graphene.Schema(query=Query) query = """ query ReporterPromiseConnectionQuery { allReporters(first: 1) { edges { node { id } } } } """ expected = {"allReporters": {"edges": [{"node": {"id": "UmVwb3J0ZXJUeXBlOjE="}}]}} result = schema.execute(query) assert not result.errors assert result.data == expected def test_should_query_connectionfields_with_last(): Reporter.objects.create( first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 ) class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) def resolve_all_reporters(self, info, **args): return Reporter.objects.all() schema = graphene.Schema(query=Query) query = """ query ReporterLastQuery { allReporters(last: 1) { edges { node { id } } } } """ expected = {"allReporters": {"edges": [{"node": {"id": "UmVwb3J0ZXJUeXBlOjE="}}]}} result = schema.execute(query) assert not result.errors assert result.data == expected def test_should_query_connectionfields_with_manager(): Reporter.objects.create( first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 ) Reporter.objects.create( first_name="John", last_name="NotDoe", email="johndoe@example.com", a_choice=1 ) class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType, on="doe_objects") def resolve_all_reporters(self, info, **args): return Reporter.objects.all() schema = graphene.Schema(query=Query) query = """ query ReporterLastQuery { allReporters(first: 1) { edges { node { id } } } } """ expected = {"allReporters": {"edges": [{"node": {"id": "UmVwb3J0ZXJUeXBlOjE="}}]}} result = schema.execute(query) assert not result.errors assert result.data == expected def test_should_query_dataloader_fields(): from promise import Promise from promise.dataloader import DataLoader def article_batch_load_fn(keys): queryset = Article.objects.filter(reporter_id__in=keys) return Promise.resolve( [ [article for article in queryset if article.reporter_id == id] for id in keys ] ) article_loader = DataLoader(article_batch_load_fn) class ArticleType(DjangoObjectType): class Meta: model = Article interfaces = (Node,) fields = "__all__" class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) use_connection = True fields = "__all__" articles = DjangoConnectionField(ArticleType) def resolve_articles(self, info, **args): return article_loader.load(self.id).get() class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) r = Reporter.objects.create( first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 ) Article.objects.create( headline="Article Node 1", pub_date=datetime.date.today(), pub_date_time=datetime.datetime.now(), reporter=r, editor=r, lang="es", ) Article.objects.create( headline="Article Node 2", pub_date=datetime.date.today(), pub_date_time=datetime.datetime.now(), reporter=r, editor=r, lang="en", ) schema = graphene.Schema(query=Query) query = """ query ReporterPromiseConnectionQuery { allReporters(first: 1) { edges { node { id articles(first: 2) { edges { node { headline } } } } } } } """ expected = { "allReporters": { "edges": [ { "node": { "id": "UmVwb3J0ZXJUeXBlOjE=", "articles": { "edges": [ {"node": {"headline": "Article Node 1"}}, {"node": {"headline": "Article Node 2"}}, ] }, } } ] } } result = schema.execute(query) assert not result.errors assert result.data == expected def test_should_handle_inherited_choices(): class BaseModel(models.Model): choice_field = models.IntegerField(choices=((0, "zero"), (1, "one"))) class ChildModel(BaseModel): class Meta: proxy = True class BaseType(DjangoObjectType): class Meta: model = BaseModel fields = "__all__" class ChildType(DjangoObjectType): class Meta: model = ChildModel fields = "__all__" class Query(graphene.ObjectType): base = graphene.Field(BaseType) child = graphene.Field(ChildType) schema = graphene.Schema(query=Query) query = """ query { child { choiceField } } """ result = schema.execute(query) assert not result.errors def test_proxy_model_support(): """ This test asserts that we can query for all Reporters and proxied Reporters. """ class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) use_connection = True fields = "__all__" class CNNReporterType(DjangoObjectType): class Meta: model = CNNReporter interfaces = (Node,) use_connection = True fields = "__all__" reporter = Reporter.objects.create( first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 ) cnn_reporter = CNNReporter.objects.create( first_name="Some", last_name="Guy", email="someguy@cnn.com", a_choice=1, reporter_type=2, # set this guy to be CNN ) class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) cnn_reporters = DjangoConnectionField(CNNReporterType) schema = graphene.Schema(query=Query) query = """ query ProxyModelQuery { allReporters { edges { node { id } } } cnnReporters { edges { node { id } } } } """ expected = { "allReporters": { "edges": [ {"node": {"id": to_global_id("ReporterType", reporter.id)}}, {"node": {"id": to_global_id("ReporterType", cnn_reporter.id)}}, ] }, "cnnReporters": { "edges": [ {"node": {"id": to_global_id("CNNReporterType", cnn_reporter.id)}} ] }, } result = schema.execute(query) assert not result.errors assert result.data == expected def test_model_inheritance_support_reverse_relationships(): """ This test asserts that we can query reverse relationships for all Reporters and proxied Reporters and multi table Reporters. """ class FilmType(DjangoObjectType): class Meta: model = Film fields = "__all__" class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) use_connection = True fields = "__all__" class CNNReporterType(DjangoObjectType): class Meta: model = CNNReporter interfaces = (Node,) use_connection = True fields = "__all__" class APNewsReporterType(DjangoObjectType): class Meta: model = APNewsReporter interfaces = (Node,) use_connection = True fields = "__all__" film = Film.objects.create(genre="do") reporter = Reporter.objects.create( first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 ) cnn_reporter = CNNReporter.objects.create( first_name="Some", last_name="Guy", email="someguy@cnn.com", a_choice=1, reporter_type=2, # set this guy to be CNN ) ap_news_reporter = APNewsReporter.objects.create( first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 ) film.reporters.add(cnn_reporter, ap_news_reporter) film.save() class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) cnn_reporters = DjangoConnectionField(CNNReporterType) ap_news_reporters = DjangoConnectionField(APNewsReporterType) schema = graphene.Schema(query=Query) query = """ query ProxyModelQuery { allReporters { edges { node { id films { id } } } } cnnReporters { edges { node { id films { id } } } } apNewsReporters { edges { node { id films { id } } } } } """ expected = { "allReporters": { "edges": [ { "node": { "id": to_global_id("ReporterType", reporter.id), "films": [], }, }, { "node": { "id": to_global_id("ReporterType", cnn_reporter.id), "films": [{"id": f"{film.id}"}], }, }, { "node": { "id": to_global_id("ReporterType", ap_news_reporter.id), "films": [{"id": f"{film.id}"}], }, }, ] }, "cnnReporters": { "edges": [ { "node": { "id": to_global_id("CNNReporterType", cnn_reporter.id), "films": [{"id": f"{film.id}"}], } } ] }, "apNewsReporters": { "edges": [ { "node": { "id": to_global_id("APNewsReporterType", ap_news_reporter.id), "films": [{"id": f"{film.id}"}], } } ] }, } result = schema.execute(query) assert result.data == expected def test_model_inheritance_support_local_relationships(): """ This test asserts that we can query local relationships for all Reporters and proxied Reporters and multi table Reporters. """ class PersonType(DjangoObjectType): class Meta: model = Person fields = "__all__" class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) use_connection = True fields = "__all__" class CNNReporterType(DjangoObjectType): class Meta: model = CNNReporter interfaces = (Node,) use_connection = True fields = "__all__" class APNewsReporterType(DjangoObjectType): class Meta: model = APNewsReporter interfaces = (Node,) use_connection = True fields = "__all__" film = Film.objects.create(genre="do") reporter = Reporter.objects.create( first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 ) reporter_fan = Person.objects.create(name="Reporter Fan") reporter.fans.add(reporter_fan) reporter.save() cnn_reporter = CNNReporter.objects.create( first_name="Some", last_name="Guy", email="someguy@cnn.com", a_choice=1, reporter_type=2, # set this guy to be CNN ) cnn_fan = Person.objects.create(name="CNN Fan") cnn_reporter.fans.add(cnn_fan) cnn_reporter.save() ap_news_reporter = APNewsReporter.objects.create( first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 ) ap_news_fan = Person.objects.create(name="AP News Fan") ap_news_reporter.fans.add(ap_news_fan) ap_news_reporter.save() film.reporters.add(cnn_reporter, ap_news_reporter) film.save() class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) cnn_reporters = DjangoConnectionField(CNNReporterType) ap_news_reporters = DjangoConnectionField(APNewsReporterType) schema = graphene.Schema(query=Query) query = """ query ProxyModelQuery { allReporters { edges { node { id fans { name } } } } cnnReporters { edges { node { id fans { name } } } } apNewsReporters { edges { node { id fans { name } } } } } """ expected = { "allReporters": { "edges": [ { "node": { "id": to_global_id("ReporterType", reporter.id), "fans": [{"name": f"{reporter_fan.name}"}], }, }, { "node": { "id": to_global_id("ReporterType", cnn_reporter.id), "fans": [{"name": f"{cnn_fan.name}"}], }, }, { "node": { "id": to_global_id("ReporterType", ap_news_reporter.id), "fans": [{"name": f"{ap_news_fan.name}"}], }, }, ] }, "cnnReporters": { "edges": [ { "node": { "id": to_global_id("CNNReporterType", cnn_reporter.id), "fans": [{"name": f"{cnn_fan.name}"}], } } ] }, "apNewsReporters": { "edges": [ { "node": { "id": to_global_id("APNewsReporterType", ap_news_reporter.id), "fans": [{"name": f"{ap_news_fan.name}"}], } } ] }, } result = schema.execute(query) assert result.data == expected def test_should_resolve_get_queryset_connectionfields(): Reporter.objects.create( first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 ) CNNReporter.objects.create( first_name="Some", last_name="Guy", email="someguy@cnn.com", a_choice=1, reporter_type=2, # set this guy to be CNN ) class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" @classmethod def get_queryset(cls, queryset, info): return queryset.filter(reporter_type=2) class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) schema = graphene.Schema(query=Query) query = """ query ReporterPromiseConnectionQuery { allReporters(first: 1) { edges { node { id } } } } """ expected = {"allReporters": {"edges": [{"node": {"id": "UmVwb3J0ZXJUeXBlOjI="}}]}} result = schema.execute(query) assert not result.errors assert result.data == expected def test_connection_should_limit_after_to_list_length(): Reporter.objects.create( first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 ) Reporter.objects.create( first_name="Some", last_name="Guy", email="someguy@cnn.com", a_choice=1 ) class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) schema = graphene.Schema(query=Query) query = """ query ReporterPromiseConnectionQuery ($after: String) { allReporters(first: 1 after: $after) { edges { node { id } } } } """ after = base64.b64encode(b"arrayconnection:10").decode() result = schema.execute(query, variable_values={"after": after}) expected = {"allReporters": {"edges": []}} assert not result.errors assert result.data == expected REPORTERS = [ { "first_name": f"First {i}", "last_name": f"Last {i}", "email": f"johndoe+{i}@example.com", "a_choice": 1, } for i in range(6) ] def test_should_return_max_limit(graphene_settings): graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 4 reporters = [Reporter(**kwargs) for kwargs in REPORTERS] Reporter.objects.bulk_create(reporters) class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) schema = graphene.Schema(query=Query) query = """ query AllReporters { allReporters { edges { node { id } } } } """ result = schema.execute(query) assert not result.errors assert len(result.data["allReporters"]["edges"]) == 4 def test_should_have_next_page(graphene_settings): graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 4 reporters = [Reporter(**kwargs) for kwargs in REPORTERS] Reporter.objects.bulk_create(reporters) db_reporters = Reporter.objects.all() class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) schema = graphene.Schema(query=Query) query = """ query AllReporters($first: Int, $after: String) { allReporters(first: $first, after: $after) { pageInfo { hasNextPage endCursor } edges { node { id } } } } """ result = schema.execute(query, variable_values={}) assert not result.errors assert len(result.data["allReporters"]["edges"]) == 4 assert result.data["allReporters"]["pageInfo"]["hasNextPage"] last_result = result.data["allReporters"]["pageInfo"]["endCursor"] result2 = schema.execute(query, variable_values={"first": 4, "after": last_result}) assert not result2.errors assert len(result2.data["allReporters"]["edges"]) == 2 assert not result2.data["allReporters"]["pageInfo"]["hasNextPage"] gql_reporters = ( result.data["allReporters"]["edges"] + result2.data["allReporters"]["edges"] ) assert {to_global_id("ReporterType", reporter.id) for reporter in db_reporters} == { gql_reporter["node"]["id"] for gql_reporter in gql_reporters } @pytest.mark.parametrize("max_limit", [100, 4]) class TestBackwardPagination: def setup_schema(self, graphene_settings, max_limit): graphene_settings.RELAY_CONNECTION_MAX_LIMIT = max_limit reporters = [Reporter(**kwargs) for kwargs in REPORTERS] Reporter.objects.bulk_create(reporters) class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) schema = graphene.Schema(query=Query) return schema def test_query_last(self, graphene_settings, max_limit): schema = self.setup_schema(graphene_settings, max_limit=max_limit) query_last = """ query { allReporters(last: 3) { edges { node { firstName } } } } """ result = schema.execute(query_last) assert not result.errors assert len(result.data["allReporters"]["edges"]) == 3 assert [ e["node"]["firstName"] for e in result.data["allReporters"]["edges"] ] == ["First 3", "First 4", "First 5"] def test_query_first_and_last(self, graphene_settings, max_limit): schema = self.setup_schema(graphene_settings, max_limit=max_limit) query_first_and_last = """ query { allReporters(first: 4, last: 3) { edges { node { firstName } } } } """ result = schema.execute(query_first_and_last) assert not result.errors assert len(result.data["allReporters"]["edges"]) == 3 assert [ e["node"]["firstName"] for e in result.data["allReporters"]["edges"] ] == ["First 1", "First 2", "First 3"] def test_query_first_last_and_after(self, graphene_settings, max_limit): schema = self.setup_schema(graphene_settings, max_limit=max_limit) query_first_last_and_after = """ query queryAfter($after: String) { allReporters(first: 4, last: 3, after: $after) { edges { node { firstName } } } } """ after = base64.b64encode(b"arrayconnection:0").decode() result = schema.execute( query_first_last_and_after, variable_values={"after": after}, ) assert not result.errors assert len(result.data["allReporters"]["edges"]) == 3 assert [ e["node"]["firstName"] for e in result.data["allReporters"]["edges"] ] == ["First 2", "First 3", "First 4"] def test_query_last_and_before(self, graphene_settings, max_limit): schema = self.setup_schema(graphene_settings, max_limit=max_limit) query_first_last_and_after = """ query queryAfter($before: String) { allReporters(last: 1, before: $before) { edges { node { firstName } } } } """ result = schema.execute( query_first_last_and_after, ) assert not result.errors assert len(result.data["allReporters"]["edges"]) == 1 assert result.data["allReporters"]["edges"][0]["node"]["firstName"] == "First 5" before = base64.b64encode(b"arrayconnection:5").decode() result = schema.execute( query_first_last_and_after, variable_values={"before": before}, ) assert not result.errors assert len(result.data["allReporters"]["edges"]) == 1 assert result.data["allReporters"]["edges"][0]["node"]["firstName"] == "First 4" def test_should_preserve_prefetch_related(django_assert_num_queries): class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (graphene.relay.Node,) fields = "__all__" class FilmType(DjangoObjectType): reporters = DjangoConnectionField(ReporterType) class Meta: model = Film interfaces = (graphene.relay.Node,) fields = "__all__" class Query(graphene.ObjectType): films = DjangoConnectionField(FilmType) def resolve_films(root, info, **kwargs): qs = Film.objects.prefetch_related("reporters") return qs r1 = Reporter.objects.create(first_name="Dave", last_name="Smith") r2 = Reporter.objects.create(first_name="Jane", last_name="Doe") f1 = Film.objects.create() f1.reporters.set([r1, r2]) f2 = Film.objects.create() f2.reporters.set([r2]) query = """ query { films { edges { node { reporters { edges { node { firstName } } } } } } } """ schema = graphene.Schema(query=Query) with django_assert_num_queries(3): result = schema.execute(query) assert not result.errors def test_should_preserve_annotations(): class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (graphene.relay.Node,) fields = "__all__" class FilmType(DjangoObjectType): reporters = DjangoConnectionField(ReporterType) reporters_count = graphene.Int() class Meta: model = Film interfaces = (graphene.relay.Node,) fields = "__all__" class Query(graphene.ObjectType): films = DjangoConnectionField(FilmType) def resolve_films(root, info, **kwargs): qs = Film.objects.prefetch_related("reporters") return qs.annotate(reporters_count=models.Count("reporters")) r1 = Reporter.objects.create(first_name="Dave", last_name="Smith") r2 = Reporter.objects.create(first_name="Jane", last_name="Doe") f1 = Film.objects.create() f1.reporters.set([r1, r2]) f2 = Film.objects.create() f2.reporters.set([r2]) query = """ query { films { edges { node { reportersCount } } } } """ schema = graphene.Schema(query=Query) result = schema.execute(query) assert not result.errors, str(result) expected = { "films": { "edges": [{"node": {"reportersCount": 2}}, {"node": {"reportersCount": 1}}] } } assert result.data == expected, str(result.data) assert not result.errors def test_connection_should_enable_offset_filtering(): Reporter.objects.create(first_name="John", last_name="Doe") Reporter.objects.create(first_name="Some", last_name="Guy") class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) schema = graphene.Schema(query=Query) query = """ query { allReporters(first: 1, offset: 1) { edges { node { firstName lastName } } } } """ result = schema.execute(query) assert not result.errors expected = { "allReporters": { "edges": [ {"node": {"firstName": "Some", "lastName": "Guy"}}, ] } } assert result.data == expected def test_connection_should_enable_offset_filtering_higher_than_max_limit( graphene_settings, ): graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 2 Reporter.objects.create(first_name="John", last_name="Doe") Reporter.objects.create(first_name="Some", last_name="Guy") Reporter.objects.create(first_name="Jane", last_name="Roe") Reporter.objects.create(first_name="Some", last_name="Lady") class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) schema = graphene.Schema(query=Query) query = """ query { allReporters(first: 1, offset: 3) { edges { node { firstName lastName } } } } """ result = schema.execute(query) assert not result.errors expected = { "allReporters": { "edges": [ {"node": {"firstName": "Some", "lastName": "Lady"}}, ] } } assert result.data == expected def test_connection_should_forbid_offset_filtering_with_before(): class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) schema = graphene.Schema(query=Query) query = """ query ReporterPromiseConnectionQuery ($before: String) { allReporters(first: 1, before: $before, offset: 1) { edges { node { firstName lastName } } } } """ before = base64.b64encode(b"arrayconnection:2").decode() result = schema.execute(query, variable_values={"before": before}) expected_error = "You can't provide a `before` value at the same time as an `offset` value to properly paginate the `allReporters` connection." assert len(result.errors) == 1 assert result.errors[0].message == expected_error def test_connection_should_allow_offset_filtering_with_after(): Reporter.objects.create(first_name="John", last_name="Doe") Reporter.objects.create(first_name="Some", last_name="Guy") Reporter.objects.create(first_name="Jane", last_name="Roe") Reporter.objects.create(first_name="Some", last_name="Lady") class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) schema = graphene.Schema(query=Query) query = """ query ReporterPromiseConnectionQuery ($after: String) { allReporters(first: 1, after: $after, offset: 1) { edges { node { firstName lastName } } } } """ after = base64.b64encode(b"arrayconnection:0").decode() result = schema.execute(query, variable_values={"after": after}) assert not result.errors expected = { "allReporters": { "edges": [ {"node": {"firstName": "Jane", "lastName": "Roe"}}, ] } } assert result.data == expected def test_connection_should_succeed_if_last_higher_than_number_of_objects(): class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node,) fields = "__all__" class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) schema = graphene.Schema(query=Query) query = """ query ReporterPromiseConnectionQuery ($last: Int) { allReporters(last: $last) { edges { node { firstName lastName } } } } """ result = schema.execute(query, variable_values={"last": 2}) assert not result.errors expected = {"allReporters": {"edges": []}} assert result.data == expected Reporter.objects.create(first_name="John", last_name="Doe") Reporter.objects.create(first_name="Some", last_name="Guy") Reporter.objects.create(first_name="Jane", last_name="Roe") Reporter.objects.create(first_name="Some", last_name="Lady") result = schema.execute(query, variable_values={"last": 2}) assert not result.errors expected = { "allReporters": { "edges": [ {"node": {"firstName": "Jane", "lastName": "Roe"}}, {"node": {"firstName": "Some", "lastName": "Lady"}}, ] } } assert result.data == expected result = schema.execute(query, variable_values={"last": 4}) assert not result.errors expected = { "allReporters": { "edges": [ {"node": {"firstName": "John", "lastName": "Doe"}}, {"node": {"firstName": "Some", "lastName": "Guy"}}, {"node": {"firstName": "Jane", "lastName": "Roe"}}, {"node": {"firstName": "Some", "lastName": "Lady"}}, ] } } assert result.data == expected result = schema.execute(query, variable_values={"last": 20}) assert not result.errors expected = { "allReporters": { "edges": [ {"node": {"firstName": "John", "lastName": "Doe"}}, {"node": {"firstName": "Some", "lastName": "Guy"}}, {"node": {"firstName": "Jane", "lastName": "Roe"}}, {"node": {"firstName": "Some", "lastName": "Lady"}}, ] } } assert result.data == expected def test_connection_should_call_resolver_function(): resolver_mock = Mock( name="resolver", return_value=[ Reporter(first_name="Some", last_name="One"), Reporter(first_name="John", last_name="Doe"), ], ) class ReporterType(DjangoObjectType): class Meta: model = Reporter fields = "__all__" interfaces = [Node] class Query(graphene.ObjectType): reporters = DjangoConnectionField(ReporterType, resolver=resolver_mock) schema = graphene.Schema(query=Query) result = schema.execute( """ query { reporters { edges { node { firstName lastName } } } } """ ) resolver_mock.assert_called_once_with(None, ANY) assert not result.errors assert result.data == { "reporters": { "edges": [ {"node": {"firstName": "Some", "lastName": "One"}}, {"node": {"firstName": "John", "lastName": "Doe"}}, ], }, } def test_should_query_nullable_foreign_key(): class PetType(DjangoObjectType): class Meta: model = Pet fields = "__all__" class PersonType(DjangoObjectType): class Meta: model = Person fields = "__all__" class Query(graphene.ObjectType): pet = graphene.Field(PetType, name=graphene.String(required=True)) person = graphene.Field(PersonType, name=graphene.String(required=True)) def resolve_pet(self, info, name): return Pet.objects.filter(name=name).first() def resolve_person(self, info, name): return Person.objects.filter(name=name).first() schema = graphene.Schema(query=Query) person = Person.objects.create(name="Jane") Pet.objects.create(name="Stray dog", age=1) Pet.objects.create(name="Jane's dog", owner=person, age=1) query_pet = """ query getPet($name: String!) { pet(name: $name) { owner { name } } } """ result = schema.execute(query_pet, variables={"name": "Stray dog"}) assert not result.errors assert result.data["pet"] == { "owner": None, } result = schema.execute(query_pet, variables={"name": "Jane's dog"}) assert not result.errors assert result.data["pet"] == { "owner": {"name": "Jane"}, } query_owner = """ query getOwner($name: String!) { person(name: $name) { pets { name } } } """ result = schema.execute(query_owner, variables={"name": "Jane"}) assert not result.errors assert result.data["person"] == { "pets": [{"name": "Jane's dog"}], } def test_should_query_nullable_one_to_one_relation_with_custom_resolver(): class FilmType(DjangoObjectType): class Meta: model = Film fields = "__all__" @classmethod def get_queryset(cls, queryset, info): return queryset class FilmDetailsType(DjangoObjectType): class Meta: model = FilmDetails fields = "__all__" @classmethod def get_queryset(cls, queryset, info): return queryset class Query(graphene.ObjectType): film = graphene.Field(FilmType, genre=graphene.String(required=True)) film_details = graphene.Field( FilmDetailsType, location=graphene.String(required=True) ) def resolve_film(self, info, genre): return Film.objects.filter(genre=genre).first() def resolve_film_details(self, info, location): return FilmDetails.objects.filter(location=location).first() schema = graphene.Schema(query=Query) Film.objects.create(genre="do") FilmDetails.objects.create(location="London") query_film = """ query getFilm($genre: String!) { film(genre: $genre) { genre details { location } } } """ query_film_details = """ query getFilmDetails($location: String!) { filmDetails(location: $location) { location film { genre } } } """ result = schema.execute(query_film, variables={"genre": "do"}) assert not result.errors assert result.data["film"] == { "genre": "DO", "details": None, } result = schema.execute(query_film_details, variables={"location": "London"}) assert not result.errors assert result.data["filmDetails"] == { "location": "London", "film": None, } django-graphene-3.2.3/graphene_django/tests/test_schema.py000066400000000000000000000026221476451357100237150ustar00rootroot00000000000000from pytest import raises from ..registry import Registry from ..types import DjangoObjectType from .models import Reporter def test_should_raise_if_no_model(): with raises(Exception) as excinfo: class Character1(DjangoObjectType): fields = "__all__" assert "valid Django Model" in str(excinfo.value) def test_should_raise_if_model_is_invalid(): with raises(Exception) as excinfo: class Character2(DjangoObjectType): class Meta: model = 1 fields = "__all__" assert "valid Django Model" in str(excinfo.value) def test_should_map_fields_correctly(): class ReporterType2(DjangoObjectType): class Meta: model = Reporter registry = Registry() fields = "__all__" fields = list(ReporterType2._meta.fields.keys()) assert fields[:-3] == [ "id", "first_name", "last_name", "email", "pets", "a_choice", "typed_choice", "class_choice", "callable_choice", "fans", "reporter_type", ] assert sorted(fields[-3:]) == ["apnewsreporter", "articles", "films"] def test_should_map_only_few_fields(): class Reporter2(DjangoObjectType): class Meta: model = Reporter fields = ("id", "email") assert list(Reporter2._meta.fields.keys()) == ["id", "email"] django-graphene-3.2.3/graphene_django/tests/test_types.py000066400000000000000000000512541476451357100236260ustar00rootroot00000000000000import warnings from collections import OrderedDict, defaultdict from textwrap import dedent from unittest.mock import patch import pytest from django.db import models from graphene import Connection, Field, Interface, ObjectType, Schema, String from graphene.relay import Node from .. import registry from ..filter import DjangoFilterConnectionField from ..types import DjangoObjectType, DjangoObjectTypeOptions from .models import ( Article as ArticleModel, Reporter as ReporterModel, ) class Reporter(DjangoObjectType): """Reporter description""" class Meta: model = ReporterModel fields = "__all__" class ArticleConnection(Connection): """Article Connection""" test = String() def resolve_test(): return "test" class Meta: abstract = True class Article(DjangoObjectType): """Article description""" class Meta: model = ArticleModel interfaces = (Node,) connection_class = ArticleConnection fields = "__all__" class RootQuery(ObjectType): node = Node.Field() schema = Schema(query=RootQuery, types=[Article, Reporter]) def test_django_interface(): assert issubclass(Node, Interface) assert issubclass(Node, Node) @patch("graphene_django.tests.models.Article.objects.get", return_value=Article(id=1)) def test_django_get_node(get): article = Article.get_node(None, 1) get.assert_called_with(pk=1) assert article.id == 1 def test_django_objecttype_map_correct_fields(): fields = Reporter._meta.fields fields = list(fields.keys()) assert fields[:-3] == [ "id", "first_name", "last_name", "email", "pets", "a_choice", "typed_choice", "class_choice", "callable_choice", "fans", "reporter_type", ] assert sorted(fields[-3:]) == ["apnewsreporter", "articles", "films"] def test_django_objecttype_with_node_have_correct_fields(): fields = Article._meta.fields assert list(fields.keys()) == [ "id", "headline", "pub_date", "pub_date_time", "reporter", "editor", "lang", "importance", ] def test_django_objecttype_with_custom_meta(): class ArticleTypeOptions(DjangoObjectTypeOptions): """Article Type Options""" class ArticleType(DjangoObjectType): class Meta: abstract = True @classmethod def __init_subclass_with_meta__(cls, **options): options.setdefault("_meta", ArticleTypeOptions(cls)) super().__init_subclass_with_meta__(**options) class Article(ArticleType): class Meta: model = ArticleModel fields = "__all__" assert isinstance(Article._meta, ArticleTypeOptions) def test_schema_representation(): expected = dedent( """\ schema { query: RootQuery } \"""Article description\""" type Article implements Node { \"""The ID of the object\""" id: ID! headline: String! pubDate: Date! pubDateTime: DateTime! reporter: Reporter! editor: Reporter! \"""Language\""" lang: TestsArticleLangChoices! importance: TestsArticleImportanceChoices } \"""An object with an ID\""" interface Node { \"""The ID of the object\""" id: ID! } \""" The `Date` scalar type represents a Date value as specified by [iso8601](https://en.wikipedia.org/wiki/ISO_8601). \""" scalar Date \""" The `DateTime` scalar type represents a DateTime value as specified by [iso8601](https://en.wikipedia.org/wiki/ISO_8601). \""" scalar DateTime \"""An enumeration.\""" enum TestsArticleLangChoices { \"""Spanish\""" ES \"""English\""" EN } \"""An enumeration.\""" enum TestsArticleImportanceChoices { \"""Very important\""" A_1 \"""Not as important\""" A_2 } \"""Reporter description\""" type Reporter { id: ID! firstName: String! lastName: String! email: String! pets: [Reporter!]! aChoice: TestsReporterAChoiceChoices typedChoice: TestsReporterTypedChoiceChoices classChoice: TestsReporterClassChoiceChoices callableChoice: TestsReporterCallableChoiceChoices reporterType: TestsReporterReporterTypeChoices articles(offset: Int, before: String, after: String, first: Int, last: Int): ArticleConnection! } \"""An enumeration.\""" enum TestsReporterAChoiceChoices { \"""this\""" A_1 \"""that\""" A_2 } \"""An enumeration.\""" enum TestsReporterTypedChoiceChoices { \"""Choice This\""" A_1 \"""Choice That\""" A_2 } \"""An enumeration.\""" enum TestsReporterClassChoiceChoices { \"""Choice This\""" A_1 \"""Choice That\""" A_2 } \"""An enumeration.\""" enum TestsReporterCallableChoiceChoices { \"""Choice This\""" THIS \"""Choice That\""" THAT } \"""An enumeration.\""" enum TestsReporterReporterTypeChoices { \"""Regular\""" A_1 \"""CNN Reporter\""" A_2 } type ArticleConnection { \"""Pagination data for this connection.\""" pageInfo: PageInfo! \"""Contains the nodes in this connection.\""" edges: [ArticleEdge]! test: String } \""" The Relay compliant `PageInfo` type, containing data necessary to paginate this connection. \""" type PageInfo { \"""When paginating forwards, are there more items?\""" hasNextPage: Boolean! \"""When paginating backwards, are there more items?\""" hasPreviousPage: Boolean! \"""When paginating backwards, the cursor to continue.\""" startCursor: String \"""When paginating forwards, the cursor to continue.\""" endCursor: String } \"""A Relay edge containing a `Article` and its cursor.\""" type ArticleEdge { \"""The item at the end of the edge\""" node: Article \"""A cursor for use in pagination\""" cursor: String! } type RootQuery { node( \"""The ID of the object\""" id: ID! ): Node }""" ) assert str(schema) == expected def with_local_registry(func): def inner(*args, **kwargs): old = registry.get_global_registry() try: retval = func(*args, **kwargs) except Exception as e: registry.registry = old raise e else: registry.registry = old return retval return inner @with_local_registry def test_django_objecttype_only_fields(): with pytest.warns(DeprecationWarning): class Reporter(DjangoObjectType): class Meta: model = ReporterModel only_fields = ("id", "email", "films") fields = list(Reporter._meta.fields.keys()) assert fields == ["id", "email", "films"] @with_local_registry def test_django_objecttype_fields(): class Reporter(DjangoObjectType): class Meta: model = ReporterModel fields = ("id", "email", "films") fields = list(Reporter._meta.fields.keys()) assert fields == ["id", "email", "films"] @with_local_registry def test_django_objecttype_fields_empty(): class Reporter(DjangoObjectType): class Meta: model = ReporterModel fields = () fields = list(Reporter._meta.fields.keys()) assert fields == [] @with_local_registry def test_django_objecttype_only_fields_and_fields(): with pytest.raises(Exception): class Reporter(DjangoObjectType): class Meta: model = ReporterModel only_fields = ("id", "email", "films") fields = ("id", "email", "films") @with_local_registry def test_django_objecttype_all_fields(): class Reporter(DjangoObjectType): class Meta: model = ReporterModel fields = "__all__" fields = list(Reporter._meta.fields.keys()) assert len(fields) == len(ReporterModel._meta.get_fields()) @with_local_registry def test_django_objecttype_exclude_fields(): with pytest.warns(DeprecationWarning): class Reporter(DjangoObjectType): class Meta: model = ReporterModel exclude_fields = ["email"] fields = list(Reporter._meta.fields.keys()) assert "email" not in fields @with_local_registry def test_django_objecttype_exclude(): class Reporter(DjangoObjectType): class Meta: model = ReporterModel exclude = ["email"] fields = list(Reporter._meta.fields.keys()) assert "email" not in fields @with_local_registry def test_django_objecttype_exclude_fields_and_exclude(): with pytest.raises(Exception): class Reporter(DjangoObjectType): class Meta: model = ReporterModel exclude = ["email"] exclude_fields = ["email"] @with_local_registry def test_django_objecttype_exclude_and_only(): with pytest.raises(AssertionError): class Reporter(DjangoObjectType): class Meta: model = ReporterModel exclude = ["email"] fields = ["id"] @with_local_registry def test_django_objecttype_fields_exclude_type_checking(): with pytest.raises(TypeError): class Reporter(DjangoObjectType): class Meta: model = ReporterModel fields = "foo" with pytest.raises(TypeError): class Reporter2(DjangoObjectType): class Meta: model = ReporterModel exclude = "foo" @with_local_registry def test_django_objecttype_fields_exist_on_model(): with pytest.warns(UserWarning, match=r"Field name .* doesn't exist"): class Reporter(DjangoObjectType): class Meta: model = ReporterModel fields = ["first_name", "foo", "email"] with pytest.warns( UserWarning, match=r"Field name .* matches an attribute on Django model .* but it's not a model field", ): class Reporter2(DjangoObjectType): class Meta: model = ReporterModel fields = ["first_name", "some_method", "email"] # Don't warn if selecting a custom field with warnings.catch_warnings(): warnings.simplefilter("error") class Reporter3(DjangoObjectType): custom_field = String() class Meta: model = ReporterModel fields = ["first_name", "custom_field", "email"] @with_local_registry def test_django_objecttype_exclude_fields_exist_on_model(): with pytest.warns( UserWarning, match=r"Django model .* does not have a field or attribute named .*", ): class Reporter(DjangoObjectType): class Meta: model = ReporterModel exclude = ["foo"] # Don't warn if selecting a custom field with pytest.warns( UserWarning, match=r"Excluding the custom field .* on DjangoObjectType .* has no effect.", ): class Reporter3(DjangoObjectType): custom_field = String() class Meta: model = ReporterModel exclude = ["custom_field"] # Don't warn on exclude fields with warnings.catch_warnings(): warnings.simplefilter("error") class Reporter4(DjangoObjectType): class Meta: model = ReporterModel exclude = ["email", "first_name"] @with_local_registry def test_django_objecttype_neither_fields_nor_exclude(): with pytest.warns( DeprecationWarning, match=r"Creating a DjangoObjectType without either the `fields` " "or the `exclude` option is deprecated.", ): class Reporter(DjangoObjectType): class Meta: model = ReporterModel with warnings.catch_warnings(): warnings.simplefilter("error") class Reporter2(DjangoObjectType): class Meta: model = ReporterModel fields = ["email"] with warnings.catch_warnings(): warnings.simplefilter("error") class Reporter3(DjangoObjectType): class Meta: model = ReporterModel exclude = ["email"] def custom_enum_name(field): return f"CustomEnum{field.name.title()}" class TestDjangoObjectType: @pytest.fixture def PetModel(self): class PetModel(models.Model): kind = models.CharField(choices=(("cat", "Cat"), ("dog", "Dog"))) cuteness = models.IntegerField( choices=((1, "Kind of cute"), (2, "Pretty cute"), (3, "OMG SO CUTE!!!")) ) yield PetModel # Clear Django model cache so we don't get warnings when creating the # model multiple times PetModel._meta.apps.all_models = defaultdict(OrderedDict) def test_django_objecttype_convert_choices_enum_false(self, PetModel): class Pet(DjangoObjectType): class Meta: model = PetModel convert_choices_to_enum = False fields = "__all__" class Query(ObjectType): pet = Field(Pet) schema = Schema(query=Query) assert str(schema) == dedent( """\ type Query { pet: Pet } type Pet { id: ID! kind: String! cuteness: Int! }""" ) def test_django_objecttype_convert_choices_enum_list(self, PetModel): class Pet(DjangoObjectType): class Meta: model = PetModel convert_choices_to_enum = ["kind"] fields = "__all__" class Query(ObjectType): pet = Field(Pet) schema = Schema(query=Query) assert str(schema) == dedent( """\ type Query { pet: Pet } type Pet { id: ID! kind: TestsPetModelKindChoices! cuteness: Int! } \"""An enumeration.\""" enum TestsPetModelKindChoices { \"""Cat\""" CAT \"""Dog\""" DOG }""" ) def test_django_objecttype_convert_choices_enum_empty_list(self, PetModel): class Pet(DjangoObjectType): class Meta: model = PetModel convert_choices_to_enum = [] fields = "__all__" class Query(ObjectType): pet = Field(Pet) schema = Schema(query=Query) assert str(schema) == dedent( """\ type Query { pet: Pet } type Pet { id: ID! kind: String! cuteness: Int! }""" ) def test_django_objecttype_convert_choices_enum_naming_collisions( self, PetModel, graphene_settings ): class PetModelKind(DjangoObjectType): class Meta: model = PetModel fields = ["id", "kind"] class Query(ObjectType): pet = Field(PetModelKind) schema = Schema(query=Query) assert str(schema) == dedent( """\ type Query { pet: PetModelKind } type PetModelKind { id: ID! kind: TestsPetModelKindChoices! } \"""An enumeration.\""" enum TestsPetModelKindChoices { \"""Cat\""" CAT \"""Dog\""" DOG }""" ) def test_django_objecttype_choices_custom_enum_name( self, PetModel, graphene_settings ): graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME = ( "graphene_django.tests.test_types.custom_enum_name" ) class PetModelKind(DjangoObjectType): class Meta: model = PetModel fields = ["id", "kind"] class Query(ObjectType): pet = Field(PetModelKind) schema = Schema(query=Query) assert str(schema) == dedent( """\ type Query { pet: PetModelKind } type PetModelKind { id: ID! kind: CustomEnumKind! } \"""An enumeration.\""" enum CustomEnumKind { \"""Cat\""" CAT \"""Dog\""" DOG }""" ) def test_django_objecttype_convert_choices_global_false( self, graphene_settings, PetModel ): graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CONVERT = False class Pet(DjangoObjectType): class Meta: model = PetModel fields = "__all__" class Query(ObjectType): pet = Field(Pet) schema = Schema(query=Query) assert str(schema) == dedent( """\ type Query { pet: Pet } type Pet { id: ID! kind: String! cuteness: Int! }""" ) def test_django_objecttype_convert_choices_true_global_false( self, graphene_settings, PetModel ): graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CONVERT = False class Pet(DjangoObjectType): class Meta: model = PetModel fields = "__all__" convert_choices_to_enum = True class Query(ObjectType): pet = Field(Pet) schema = Schema(query=Query) assert str(schema) == dedent( """\ type Query { pet: Pet } type Pet { id: ID! kind: TestsPetModelKindChoices! cuteness: TestsPetModelCutenessChoices! } \"""An enumeration.\""" enum TestsPetModelKindChoices { \"""Cat\""" CAT \"""Dog\""" DOG } \"""An enumeration.\""" enum TestsPetModelCutenessChoices { \"""Kind of cute\""" A_1 \"""Pretty cute\""" A_2 \"""OMG SO CUTE!!!\""" A_3 }""" ) def test_django_objecttype_convert_choices_enum_list_global_false( self, graphene_settings, PetModel ): graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CONVERT = False class Pet(DjangoObjectType): class Meta: model = PetModel convert_choices_to_enum = ["kind"] fields = "__all__" class Query(ObjectType): pet = Field(Pet) schema = Schema(query=Query) assert str(schema) == dedent( """\ type Query { pet: Pet } type Pet { id: ID! kind: TestsPetModelKindChoices! cuteness: Int! } \"""An enumeration.\""" enum TestsPetModelKindChoices { \"""Cat\""" CAT \"""Dog\""" DOG }""" ) @with_local_registry def test_django_objecttype_name_connection_propagation(): class Reporter(DjangoObjectType): class Meta: model = ReporterModel name = "CustomReporterName" fields = "__all__" filter_fields = ["email"] interfaces = (Node,) class Query(ObjectType): reporter = Node.Field(Reporter) reporters = DjangoFilterConnectionField(Reporter) assert Reporter._meta.name == "CustomReporterName" schema = str(Schema(query=Query)) assert "type CustomReporterName implements Node {" in schema assert "type CustomReporterNameConnection {" in schema assert "type CustomReporterNameEdge {" in schema assert "type Reporter implements Node {" not in schema assert "type ReporterConnection {" not in schema assert "type ReporterEdge {" not in schema django-graphene-3.2.3/graphene_django/tests/test_utils.py000066400000000000000000000067061476451357100236240ustar00rootroot00000000000000import json from unittest.mock import patch import pytest from django.utils.translation import gettext_lazy from ..utils import GraphQLTestCase, camelize, get_model_fields, get_reverse_fields from ..utils.testing import graphql_query from .models import APNewsReporter, CNNReporter, Film, Reporter def test_get_model_fields_no_duplication(): reporter_fields = get_model_fields(Reporter) reporter_name_set = {field[0] for field in reporter_fields} assert len(reporter_fields) == len(reporter_name_set) film_fields = get_model_fields(Film) film_name_set = {field[0] for field in film_fields} assert len(film_fields) == len(film_name_set) def test_get_reverse_fields_includes_proxied_models(): reporter_fields = get_reverse_fields(Reporter, []) cnn_reporter_fields = get_reverse_fields(CNNReporter, []) ap_news_reporter_fields = get_reverse_fields(APNewsReporter, []) assert ( len(list(reporter_fields)) == len(list(cnn_reporter_fields)) == len(list(ap_news_reporter_fields)) ) def test_camelize(): assert camelize({}) == {} assert camelize("value_a") == "value_a" assert camelize({"value_a": "value_b"}) == {"valueA": "value_b"} assert camelize({"value_a": ["value_b"]}) == {"valueA": ["value_b"]} assert camelize({"value_a": ["value_b"]}) == {"valueA": ["value_b"]} assert camelize({"nested_field": {"value_a": ["error"], "value_b": ["error"]}}) == { "nestedField": {"valueA": ["error"], "valueB": ["error"]} } assert camelize({"value_a": gettext_lazy("value_b")}) == {"valueA": "value_b"} assert camelize({"value_a": [gettext_lazy("value_b")]}) == {"valueA": ["value_b"]} assert camelize(gettext_lazy("value_a")) == "value_a" assert camelize({gettext_lazy("value_a"): gettext_lazy("value_b")}) == { "valueA": "value_b" } assert camelize({0: {"field_a": ["errors"]}}) == {0: {"fieldA": ["errors"]}} @pytest.mark.django_db @patch("graphene_django.utils.testing.Client.post") def test_graphql_test_case_operation_name(post_mock): """ Test that `GraphQLTestCase.query()`'s `operation_name` argument produces an `operationName` field. """ class TestClass(GraphQLTestCase): GRAPHQL_SCHEMA = True def runTest(self): pass tc = TestClass() tc._pre_setup() tc.setUpClass() tc.query("query { }", operation_name="QueryName") body = json.loads(post_mock.call_args[0][1]) # `operationName` field from https://graphql.org/learn/serving-over-http/#post-request assert ( "operationName", "QueryName", ) in body.items(), "Field 'operationName' is not present in the final request." @pytest.mark.django_db @patch("graphene_django.utils.testing.Client.post") def test_graphql_query_case_operation_name(post_mock): graphql_query("query { }", operation_name="QueryName") body = json.loads(post_mock.call_args[0][1]) # `operationName` field from https://graphql.org/learn/serving-over-http/#post-request assert ( "operationName", "QueryName", ) in body.items(), "Field 'operationName' is not present in the final request." @pytest.fixture def client_query(client): def func(*args, **kwargs): return graphql_query(*args, client=client, **kwargs) return func def test_pytest_fixture_usage(client_query): response = client_query("query { test }") content = json.loads(response.content) assert content == {"data": {"test": "Hello World"}} django-graphene-3.2.3/graphene_django/tests/test_views.py000066400000000000000000000654471476451357100236300ustar00rootroot00000000000000import json from http import HTTPStatus from unittest.mock import patch import pytest from django.db import connection from .models import Pet try: from urllib import urlencode except ImportError: from urllib.parse import urlencode def url_string(string="/graphql", **url_params): if url_params: string += "?" + urlencode(url_params) return string def batch_url_string(**url_params): return url_string("/graphql/batch", **url_params) def response_json(response): return json.loads(response.content.decode()) def j(**kwargs): return json.dumps(kwargs) def jl(**kwargs): return json.dumps([kwargs]) def test_graphiql_is_enabled(client): response = client.get(url_string(), HTTP_ACCEPT="text/html") assert response.status_code == HTTPStatus.OK assert response["Content-Type"].split(";")[0] == "text/html" def test_qfactor_graphiql(client): response = client.get( url_string(query="{test}"), HTTP_ACCEPT="application/json;q=0.8, text/html;q=0.9", ) assert response.status_code == HTTPStatus.OK assert response["Content-Type"].split(";")[0] == "text/html" def test_qfactor_json(client): response = client.get( url_string(query="{test}"), HTTP_ACCEPT="text/html;q=0.8, application/json;q=0.9", ) assert response.status_code == HTTPStatus.OK assert response["Content-Type"].split(";")[0] == "application/json" assert response_json(response) == {"data": {"test": "Hello World"}} def test_allows_get_with_query_param(client): response = client.get(url_string(query="{test}")) assert response.status_code == HTTPStatus.OK assert response_json(response) == {"data": {"test": "Hello World"}} def test_allows_get_with_variable_values(client): response = client.get( url_string( query="query helloWho($who: String){ test(who: $who) }", variables=json.dumps({"who": "Dolly"}), ) ) assert response.status_code == HTTPStatus.OK assert response_json(response) == {"data": {"test": "Hello Dolly"}} def test_allows_get_with_operation_name(client): response = client.get( url_string( query=""" query helloYou { test(who: "You"), ...shared } query helloWorld { test(who: "World"), ...shared } query helloDolly { test(who: "Dolly"), ...shared } fragment shared on QueryRoot { shared: test(who: "Everyone") } """, operationName="helloWorld", ) ) assert response.status_code == HTTPStatus.OK assert response_json(response) == { "data": {"test": "Hello World", "shared": "Hello Everyone"} } def test_reports_validation_errors(client): response = client.get(url_string(query="{ test, unknownOne, unknownTwo }")) assert response.status_code == HTTPStatus.BAD_REQUEST assert response_json(response) == { "errors": [ { "message": "Cannot query field 'unknownOne' on type 'QueryRoot'.", "locations": [{"line": 1, "column": 9}], }, { "message": "Cannot query field 'unknownTwo' on type 'QueryRoot'.", "locations": [{"line": 1, "column": 21}], }, ] } def test_errors_when_missing_operation_name(client): response = client.get( url_string( query=""" query TestQuery { test } mutation TestMutation { writeTest { test } } """ ) ) assert response.status_code == HTTPStatus.BAD_REQUEST assert response_json(response) == { "errors": [ { "message": "Must provide operation name if query contains multiple operations.", } ] } def test_errors_when_sending_a_mutation_via_get(client): response = client.get( url_string( query=""" mutation TestMutation { writeTest { test } } """ ) ) assert response.status_code == HTTPStatus.METHOD_NOT_ALLOWED assert response_json(response) == { "errors": [ {"message": "Can only perform a mutation operation from a POST request."} ] } def test_errors_when_selecting_a_mutation_within_a_get(client): response = client.get( url_string( query=""" query TestQuery { test } mutation TestMutation { writeTest { test } } """, operationName="TestMutation", ) ) assert response.status_code == HTTPStatus.METHOD_NOT_ALLOWED assert response_json(response) == { "errors": [ {"message": "Can only perform a mutation operation from a POST request."} ] } def test_allows_mutation_to_exist_within_a_get(client): response = client.get( url_string( query=""" query TestQuery { test } mutation TestMutation { writeTest { test } } """, operationName="TestQuery", ) ) assert response.status_code == HTTPStatus.OK assert response_json(response) == {"data": {"test": "Hello World"}} def test_allows_post_with_json_encoding(client): response = client.post(url_string(), j(query="{test}"), "application/json") assert response.status_code == HTTPStatus.OK assert response_json(response) == {"data": {"test": "Hello World"}} def test_batch_allows_post_with_json_encoding(client): response = client.post( batch_url_string(), jl(id=1, query="{test}"), "application/json" ) assert response.status_code == HTTPStatus.OK assert response_json(response) == [ {"id": 1, "data": {"test": "Hello World"}, "status": 200} ] def test_batch_fails_if_is_empty(client): response = client.post(batch_url_string(), "[]", "application/json") assert response.status_code == HTTPStatus.BAD_REQUEST assert response_json(response) == { "errors": [{"message": "Received an empty list in the batch request."}] } def test_allows_sending_a_mutation_via_post(client): response = client.post( url_string(), j(query="mutation TestMutation { writeTest { test } }"), "application/json", ) assert response.status_code == HTTPStatus.OK assert response_json(response) == {"data": {"writeTest": {"test": "Hello World"}}} def test_allows_post_with_url_encoding(client): response = client.post( url_string(), urlencode({"query": "{test}"}), "application/x-www-form-urlencoded", ) assert response.status_code == HTTPStatus.OK assert response_json(response) == {"data": {"test": "Hello World"}} def test_supports_post_json_query_with_string_variables(client): response = client.post( url_string(), j( query="query helloWho($who: String){ test(who: $who) }", variables=json.dumps({"who": "Dolly"}), ), "application/json", ) assert response.status_code == HTTPStatus.OK assert response_json(response) == {"data": {"test": "Hello Dolly"}} def test_batch_supports_post_json_query_with_string_variables(client): response = client.post( batch_url_string(), jl( id=1, query="query helloWho($who: String){ test(who: $who) }", variables=json.dumps({"who": "Dolly"}), ), "application/json", ) assert response.status_code == HTTPStatus.OK assert response_json(response) == [ {"id": 1, "data": {"test": "Hello Dolly"}, "status": 200} ] def test_supports_post_json_query_with_json_variables(client): response = client.post( url_string(), j( query="query helloWho($who: String){ test(who: $who) }", variables={"who": "Dolly"}, ), "application/json", ) assert response.status_code == HTTPStatus.OK assert response_json(response) == {"data": {"test": "Hello Dolly"}} def test_batch_supports_post_json_query_with_json_variables(client): response = client.post( batch_url_string(), jl( id=1, query="query helloWho($who: String){ test(who: $who) }", variables={"who": "Dolly"}, ), "application/json", ) assert response.status_code == HTTPStatus.OK assert response_json(response) == [ {"id": 1, "data": {"test": "Hello Dolly"}, "status": 200} ] def test_supports_post_url_encoded_query_with_string_variables(client): response = client.post( url_string(), urlencode( { "query": "query helloWho($who: String){ test(who: $who) }", "variables": json.dumps({"who": "Dolly"}), } ), "application/x-www-form-urlencoded", ) assert response.status_code == HTTPStatus.OK assert response_json(response) == {"data": {"test": "Hello Dolly"}} def test_supports_post_json_quey_with_get_variable_values(client): response = client.post( url_string(variables=json.dumps({"who": "Dolly"})), j(query="query helloWho($who: String){ test(who: $who) }"), "application/json", ) assert response.status_code == HTTPStatus.OK assert response_json(response) == {"data": {"test": "Hello Dolly"}} def test_post_url_encoded_query_with_get_variable_values(client): response = client.post( url_string(variables=json.dumps({"who": "Dolly"})), urlencode({"query": "query helloWho($who: String){ test(who: $who) }"}), "application/x-www-form-urlencoded", ) assert response.status_code == HTTPStatus.OK assert response_json(response) == {"data": {"test": "Hello Dolly"}} def test_supports_post_raw_text_query_with_get_variable_values(client): response = client.post( url_string(variables=json.dumps({"who": "Dolly"})), "query helloWho($who: String){ test(who: $who) }", "application/graphql", ) assert response.status_code == HTTPStatus.OK assert response_json(response) == {"data": {"test": "Hello Dolly"}} def test_allows_post_with_operation_name(client): response = client.post( url_string(), j( query=""" query helloYou { test(who: "You"), ...shared } query helloWorld { test(who: "World"), ...shared } query helloDolly { test(who: "Dolly"), ...shared } fragment shared on QueryRoot { shared: test(who: "Everyone") } """, operationName="helloWorld", ), "application/json", ) assert response.status_code == HTTPStatus.OK assert response_json(response) == { "data": {"test": "Hello World", "shared": "Hello Everyone"} } def test_batch_allows_post_with_operation_name(client): response = client.post( batch_url_string(), jl( id=1, query=""" query helloYou { test(who: "You"), ...shared } query helloWorld { test(who: "World"), ...shared } query helloDolly { test(who: "Dolly"), ...shared } fragment shared on QueryRoot { shared: test(who: "Everyone") } """, operationName="helloWorld", ), "application/json", ) assert response.status_code == HTTPStatus.OK assert response_json(response) == [ { "id": 1, "data": {"test": "Hello World", "shared": "Hello Everyone"}, "status": 200, } ] def test_allows_post_with_get_operation_name(client): response = client.post( url_string(operationName="helloWorld"), """ query helloYou { test(who: "You"), ...shared } query helloWorld { test(who: "World"), ...shared } query helloDolly { test(who: "Dolly"), ...shared } fragment shared on QueryRoot { shared: test(who: "Everyone") } """, "application/graphql", ) assert response.status_code == HTTPStatus.OK assert response_json(response) == { "data": {"test": "Hello World", "shared": "Hello Everyone"} } @pytest.mark.urls("graphene_django.tests.urls_inherited") def test_inherited_class_with_attributes_works(client): inherited_url = "/graphql/inherited/" # Check schema and pretty attributes work response = client.post(url_string(inherited_url, query="{test}")) assert response.content.decode() == ( "{\n" ' "data": {\n' ' "test": "Hello World"\n' " }\n" "}" ) # Check graphiql works response = client.get(url_string(inherited_url), HTTP_ACCEPT="text/html") assert response.status_code == HTTPStatus.OK @pytest.mark.urls("graphene_django.tests.urls_pretty") def test_supports_pretty_printing(client): response = client.get(url_string(query="{test}")) assert response.content.decode() == ( "{\n" ' "data": {\n' ' "test": "Hello World"\n' " }\n" "}" ) def test_supports_pretty_printing_by_request(client): response = client.get(url_string(query="{test}", pretty="1")) assert response.content.decode() == ( "{\n" ' "data": {\n' ' "test": "Hello World"\n' " }\n" "}" ) def test_handles_field_errors_caught_by_graphql(client): response = client.get(url_string(query="{thrower}")) assert response.status_code == HTTPStatus.OK assert response_json(response) == { "data": None, "errors": [ { "locations": [{"column": 2, "line": 1}], "path": ["thrower"], "message": "Throws!", } ], } def test_handles_syntax_errors_caught_by_graphql(client): response = client.get(url_string(query="syntaxerror")) assert response.status_code == HTTPStatus.BAD_REQUEST assert response_json(response) == { "errors": [ { "locations": [{"column": 1, "line": 1}], "message": "Syntax Error: Unexpected Name 'syntaxerror'.", } ] } def test_handles_errors_caused_by_a_lack_of_query(client): response = client.get(url_string()) assert response.status_code == HTTPStatus.BAD_REQUEST assert response_json(response) == { "errors": [{"message": "Must provide query string."}] } def test_handles_not_expected_json_bodies(client): response = client.post(url_string(), "[]", "application/json") assert response.status_code == HTTPStatus.BAD_REQUEST assert response_json(response) == { "errors": [{"message": "The received data is not a valid JSON query."}] } def test_handles_invalid_json_bodies(client): response = client.post(url_string(), "[oh}", "application/json") assert response.status_code == HTTPStatus.BAD_REQUEST assert response_json(response) == { "errors": [{"message": "POST body sent invalid JSON."}] } def test_handles_django_request_error(client, monkeypatch): def mocked_read(*args): raise OSError("foo-bar") monkeypatch.setattr("django.http.request.HttpRequest.read", mocked_read) valid_json = json.dumps({"foo": "bar"}) response = client.post(url_string(), valid_json, "application/json") assert response.status_code == HTTPStatus.BAD_REQUEST assert response_json(response) == {"errors": [{"message": "foo-bar"}]} def test_handles_incomplete_json_bodies(client): response = client.post(url_string(), '{"query":', "application/json") assert response.status_code == HTTPStatus.BAD_REQUEST assert response_json(response) == { "errors": [{"message": "POST body sent invalid JSON."}] } def test_handles_plain_post_text(client): response = client.post( url_string(variables=json.dumps({"who": "Dolly"})), "query helloWho($who: String){ test(who: $who) }", "text/plain", ) assert response.status_code == HTTPStatus.BAD_REQUEST assert response_json(response) == { "errors": [{"message": "Must provide query string."}] } def test_handles_poorly_formed_variables(client): response = client.get( url_string( query="query helloWho($who: String){ test(who: $who) }", variables="who:You" ) ) assert response.status_code == HTTPStatus.BAD_REQUEST assert response_json(response) == { "errors": [{"message": "Variables are invalid JSON."}] } def test_handles_unsupported_http_methods(client): response = client.put(url_string(query="{test}")) assert response.status_code == HTTPStatus.METHOD_NOT_ALLOWED assert response["Allow"] == "GET, POST" assert response_json(response) == { "errors": [{"message": "GraphQL only supports GET and POST requests."}] } def test_passes_request_into_context_request(client): response = client.get(url_string(query="{request}", q="testing")) assert response.status_code == HTTPStatus.OK assert response_json(response) == {"data": {"request": "testing"}} @patch("graphene_django.settings.graphene_settings.ATOMIC_MUTATIONS", False) @patch.dict( connection.settings_dict, {"ATOMIC_MUTATIONS": False, "ATOMIC_REQUESTS": True} ) def test_form_mutation_multiple_creation_invalid_atomic_request(client): query = """ mutation PetMutations { petFormMutation1: petFormMutation(input: { name: "Mia", age: 99 }) { errors { field messages } } petFormMutation2: petFormMutation(input: { name: "Enzo", age: 0 }) { errors { field messages } } } """ response = client.post(url_string(query=query)) content = response_json(response) assert "errors" not in content assert content["data"]["petFormMutation1"]["errors"] == [ {"field": "age", "messages": ["Too old"]} ] assert content["data"]["petFormMutation2"]["errors"] == [] assert Pet.objects.count() == 0 @patch("graphene_django.settings.graphene_settings.ATOMIC_MUTATIONS", False) @patch.dict( connection.settings_dict, {"ATOMIC_MUTATIONS": True, "ATOMIC_REQUESTS": False} ) def test_form_mutation_multiple_creation_invalid_atomic_mutation_1(client): query = """ mutation PetMutations { petFormMutation1: petFormMutation(input: { name: "Mia", age: 99 }) { errors { field messages } } petFormMutation2: petFormMutation(input: { name: "Enzo", age: 0 }) { errors { field messages } } } """ response = client.post(url_string(query=query)) content = response_json(response) assert "errors" not in content assert content["data"]["petFormMutation1"]["errors"] == [ {"field": "age", "messages": ["Too old"]} ] assert content["data"]["petFormMutation2"]["errors"] == [] assert Pet.objects.count() == 0 @patch("graphene_django.settings.graphene_settings.ATOMIC_MUTATIONS", True) @patch.dict( connection.settings_dict, {"ATOMIC_MUTATIONS": False, "ATOMIC_REQUESTS": False} ) def test_form_mutation_multiple_creation_invalid_atomic_mutation_2(client): query = """ mutation PetMutations { petFormMutation1: petFormMutation(input: { name: "Mia", age: 99 }) { errors { field messages } } petFormMutation2: petFormMutation(input: { name: "Enzo", age: 0 }) { errors { field messages } } } """ response = client.post(url_string(query=query)) content = response_json(response) assert "errors" not in content assert content["data"]["petFormMutation1"]["errors"] == [ {"field": "age", "messages": ["Too old"]} ] assert content["data"]["petFormMutation2"]["errors"] == [] assert Pet.objects.count() == 0 @patch("graphene_django.settings.graphene_settings.ATOMIC_MUTATIONS", False) @patch.dict( connection.settings_dict, {"ATOMIC_MUTATIONS": False, "ATOMIC_REQUESTS": False} ) def test_form_mutation_multiple_creation_invalid_non_atomic(client): query = """ mutation PetMutations { petFormMutation1: petFormMutation(input: { name: "Mia", age: 99 }) { errors { field messages } } petFormMutation2: petFormMutation(input: { name: "Enzo", age: 0 }) { errors { field messages } } } """ response = client.post(url_string(query=query)) content = response_json(response) assert "errors" not in content assert content["data"]["petFormMutation1"]["errors"] == [ {"field": "age", "messages": ["Too old"]} ] assert content["data"]["petFormMutation2"]["errors"] == [] assert Pet.objects.count() == 1 pet = Pet.objects.get() assert pet.name == "Enzo" assert pet.age == 0 @patch("graphene_django.settings.graphene_settings.ATOMIC_MUTATIONS", False) @patch.dict( connection.settings_dict, {"ATOMIC_MUTATIONS": False, "ATOMIC_REQUESTS": True} ) def test_model_form_mutation_multiple_creation_invalid_atomic_request(client): query = """ mutation PetMutations { petMutation1: petMutation(input: { name: "Mia", age: 99 }) { pet { name age } errors { field messages } } petMutation2: petMutation(input: { name: "Enzo", age: 0 }) { pet { name age } errors { field messages } } } """ response = client.post(url_string(query=query)) content = response_json(response) assert "errors" not in content assert content["data"]["petMutation1"]["pet"] is None assert content["data"]["petMutation1"]["errors"] == [ {"field": "age", "messages": ["Too old"]} ] assert content["data"]["petMutation2"]["pet"] == {"name": "Enzo", "age": 0} assert Pet.objects.count() == 0 @patch("graphene_django.settings.graphene_settings.ATOMIC_MUTATIONS", False) @patch.dict( connection.settings_dict, {"ATOMIC_MUTATIONS": False, "ATOMIC_REQUESTS": False} ) def test_model_form_mutation_multiple_creation_invalid_non_atomic(client): query = """ mutation PetMutations { petMutation1: petMutation(input: { name: "Mia", age: 99 }) { pet { name age } errors { field messages } } petMutation2: petMutation(input: { name: "Enzo", age: 0 }) { pet { name age } errors { field messages } } } """ response = client.post(url_string(query=query)) content = response_json(response) assert "errors" not in content assert content["data"]["petMutation1"]["pet"] is None assert content["data"]["petMutation1"]["errors"] == [ {"field": "age", "messages": ["Too old"]} ] assert content["data"]["petMutation2"]["pet"] == {"name": "Enzo", "age": 0} assert Pet.objects.count() == 1 pet = Pet.objects.get() assert pet.name == "Enzo" assert pet.age == 0 @patch("graphene_django.utils.utils.transaction.set_rollback") @patch("graphene_django.settings.graphene_settings.ATOMIC_MUTATIONS", False) @patch.dict( connection.settings_dict, {"ATOMIC_MUTATIONS": False, "ATOMIC_REQUESTS": True} ) def test_query_errors_atomic_request(set_rollback_mock, client): client.get(url_string(query="force error")) set_rollback_mock.assert_called_once_with(True) @patch("graphene_django.utils.utils.transaction.set_rollback") @patch("graphene_django.settings.graphene_settings.ATOMIC_MUTATIONS", False) @patch.dict( connection.settings_dict, {"ATOMIC_MUTATIONS": False, "ATOMIC_REQUESTS": False} ) def test_query_errors_non_atomic(set_rollback_mock, client): client.get(url_string(query="force error")) set_rollback_mock.assert_not_called() VALIDATION_URLS = [ "/graphql/validation/", "/graphql/validation/alternative/", "/graphql/validation/inherited/", ] QUERY_WITH_TWO_INTROSPECTIONS = """ query Instrospection { queryType: __schema { queryType {name} } mutationType: __schema { mutationType {name} } } """ N_INTROSPECTIONS = 2 INTROSPECTION_DISALLOWED_ERROR_MESSAGE = "introspection is disabled" MAX_VALIDATION_ERRORS_EXCEEDED_MESSAGE = "too many validation errors" @pytest.mark.urls("graphene_django.tests.urls_validation") def test_allow_introspection(client): response = client.post( url_string("/graphql/", query="{__schema {queryType {name}}}") ) assert response.status_code == HTTPStatus.OK assert response_json(response) == { "data": {"__schema": {"queryType": {"name": "QueryRoot"}}} } @pytest.mark.parametrize("url", VALIDATION_URLS) @pytest.mark.urls("graphene_django.tests.urls_validation") def test_validation_disallow_introspection(client, url): response = client.post(url_string(url, query="{__schema {queryType {name}}}")) assert response.status_code == HTTPStatus.BAD_REQUEST json_response = response_json(response) assert "data" not in json_response assert "errors" in json_response assert len(json_response["errors"]) == 1 error_message = json_response["errors"][0]["message"] assert INTROSPECTION_DISALLOWED_ERROR_MESSAGE in error_message @pytest.mark.parametrize("url", VALIDATION_URLS) @pytest.mark.urls("graphene_django.tests.urls_validation") @patch( "graphene_django.settings.graphene_settings.MAX_VALIDATION_ERRORS", N_INTROSPECTIONS ) def test_within_max_validation_errors(client, url): response = client.post(url_string(url, query=QUERY_WITH_TWO_INTROSPECTIONS)) assert response.status_code == HTTPStatus.BAD_REQUEST json_response = response_json(response) assert "data" not in json_response assert "errors" in json_response assert len(json_response["errors"]) == N_INTROSPECTIONS error_messages = [error["message"].lower() for error in json_response["errors"]] n_introspection_error_messages = sum( INTROSPECTION_DISALLOWED_ERROR_MESSAGE in msg for msg in error_messages ) assert n_introspection_error_messages == N_INTROSPECTIONS assert all( MAX_VALIDATION_ERRORS_EXCEEDED_MESSAGE not in msg for msg in error_messages ) @pytest.mark.parametrize("url", VALIDATION_URLS) @pytest.mark.urls("graphene_django.tests.urls_validation") @patch("graphene_django.settings.graphene_settings.MAX_VALIDATION_ERRORS", 1) def test_exceeds_max_validation_errors(client, url): response = client.post(url_string(url, query=QUERY_WITH_TWO_INTROSPECTIONS)) assert response.status_code == HTTPStatus.BAD_REQUEST json_response = response_json(response) assert "data" not in json_response assert "errors" in json_response error_messages = (error["message"].lower() for error in json_response["errors"]) assert any(MAX_VALIDATION_ERRORS_EXCEEDED_MESSAGE in msg for msg in error_messages) django-graphene-3.2.3/graphene_django/tests/types.py000066400000000000000000000002561476451357100225630ustar00rootroot00000000000000from graphene_django.types import DjangoObjectType from .models import Pet class PetType(DjangoObjectType): class Meta: model = Pet fields = "__all__" django-graphene-3.2.3/graphene_django/tests/urls.py000066400000000000000000000003061476451357100224000ustar00rootroot00000000000000from django.urls import path from ..views import GraphQLView urlpatterns = [ path("graphql/batch", GraphQLView.as_view(batch=True)), path("graphql", GraphQLView.as_view(graphiql=True)), ] django-graphene-3.2.3/graphene_django/tests/urls_inherited.py000066400000000000000000000004121476451357100244310ustar00rootroot00000000000000from django.urls import path from ..views import GraphQLView from .schema_view import schema class CustomGraphQLView(GraphQLView): schema = schema graphiql = True pretty = True urlpatterns = [path("graphql/inherited/", CustomGraphQLView.as_view())] django-graphene-3.2.3/graphene_django/tests/urls_pretty.py000066400000000000000000000002601476451357100240060ustar00rootroot00000000000000from django.urls import path from ..views import GraphQLView from .schema_view import schema urlpatterns = [path("graphql", GraphQLView.as_view(schema=schema, pretty=True))] django-graphene-3.2.3/graphene_django/tests/urls_validation.py000066400000000000000000000012131476451357100246100ustar00rootroot00000000000000from django.urls import path from graphene.validation import DisableIntrospection from ..views import GraphQLView from .schema_view import schema class View(GraphQLView): schema = schema class NoIntrospectionView(View): validation_rules = (DisableIntrospection,) class NoIntrospectionViewInherited(NoIntrospectionView): pass urlpatterns = [ path("graphql/", View.as_view()), path("graphql/validation/", View.as_view(validation_rules=(DisableIntrospection,))), path("graphql/validation/alternative/", NoIntrospectionView.as_view()), path("graphql/validation/inherited/", NoIntrospectionViewInherited.as_view()), ] django-graphene-3.2.3/graphene_django/types.py000066400000000000000000000252131476451357100214210ustar00rootroot00000000000000import warnings from collections import OrderedDict from typing import Type # noqa: F401 from django.db.models import Model # noqa: F401 import graphene from graphene.relay import Connection, Node from graphene.types.objecttype import ObjectType, ObjectTypeOptions from graphene.types.utils import yank_fields_from_attrs from .converter import convert_django_field_with_choices from .registry import Registry, get_global_registry from .settings import graphene_settings from .utils import ( DJANGO_FILTER_INSTALLED, camelize, get_model_fields, is_valid_django_model, ) ALL_FIELDS = "__all__" def construct_fields( model, registry, only_fields, exclude_fields, convert_choices_to_enum=None ): _model_fields = get_model_fields(model) fields = OrderedDict() for name, field in _model_fields: is_not_in_only = ( only_fields is not None and only_fields != ALL_FIELDS and name not in only_fields ) # is_already_created = name in options.fields is_excluded = ( exclude_fields is not None and name in exclude_fields ) # or is_already_created # https://docs.djangoproject.com/en/1.10/ref/models/fields/#django.db.models.ForeignKey.related_query_name is_no_backref = str(name).endswith("+") if is_not_in_only or is_excluded or is_no_backref: # We skip this field if we specify only_fields and is not # in there. Or when we exclude this field in exclude_fields. # Or when there is no back reference. continue _convert_choices_to_enum = convert_choices_to_enum if isinstance(_convert_choices_to_enum, list): # then `convert_choices_to_enum` is a list of field names to convert if name in _convert_choices_to_enum: _convert_choices_to_enum = True else: _convert_choices_to_enum = False converted = convert_django_field_with_choices( field, registry, convert_choices_to_enum=_convert_choices_to_enum ) fields[name] = converted return fields def validate_fields(type_, model, fields, only_fields, exclude_fields): # Validate the given fields against the model's fields and custom fields all_field_names = set(fields.keys()) only_fields = only_fields if only_fields is not ALL_FIELDS else () for name in only_fields or (): if name in all_field_names: continue if hasattr(model, name): warnings.warn( ( 'Field name "{field_name}" matches an attribute on Django model "{app_label}.{object_name}" ' "but it's not a model field so Graphene cannot determine what type it should be. " 'Either define the type of the field on DjangoObjectType "{type_}" or remove it from the "fields" list.' ).format( field_name=name, app_label=model._meta.app_label, object_name=model._meta.object_name, type_=type_, ) ) else: warnings.warn( ( 'Field name "{field_name}" doesn\'t exist on Django model "{app_label}.{object_name}". ' 'Consider removing the field from the "fields" list of DjangoObjectType "{type_}" because it has no effect.' ).format( field_name=name, app_label=model._meta.app_label, object_name=model._meta.object_name, type_=type_, ) ) # Validate exclude fields for name in exclude_fields or (): if name in all_field_names: # Field is a custom field warnings.warn( f'Excluding the custom field "{name}" on DjangoObjectType "{type_}" has no effect. ' 'Either remove the custom field or remove the field from the "exclude" list.' ) else: if not hasattr(model, name): warnings.warn( ( 'Django model "{app_label}.{object_name}" does not have a field or attribute named "{field_name}". ' 'Consider removing the field from the "exclude" list of DjangoObjectType "{type_}" because it has no effect' ).format( field_name=name, app_label=model._meta.app_label, object_name=model._meta.object_name, type_=type_, ) ) class DjangoObjectTypeOptions(ObjectTypeOptions): model = None # type: Type[Model] registry = None # type: Registry connection = None # type: Type[Connection] filter_fields = () filterset_class = None class DjangoObjectType(ObjectType): @classmethod def __init_subclass_with_meta__( cls, model=None, registry=None, skip_registry=False, only_fields=None, # deprecated in favour of `fields` fields=None, exclude_fields=None, # deprecated in favour of `exclude` exclude=None, filter_fields=None, filterset_class=None, connection=None, connection_class=None, use_connection=None, interfaces=(), convert_choices_to_enum=None, _meta=None, **options, ): assert is_valid_django_model(model), ( 'You need to pass a valid Django Model in {}.Meta, received "{}".' ).format(cls.__name__, model) if not registry: registry = get_global_registry() assert isinstance(registry, Registry), ( f"The attribute registry in {cls.__name__} needs to be an instance of " f'Registry, received "{registry}".' ) if filter_fields and filterset_class: raise Exception("Can't set both filter_fields and filterset_class") if not DJANGO_FILTER_INSTALLED and (filter_fields or filterset_class): raise Exception( "Can only set filter_fields or filterset_class if " "Django-Filter is installed" ) assert not (fields and exclude), ( "Cannot set both 'fields' and 'exclude' options on " f"DjangoObjectType {cls.__name__}." ) # Alias only_fields -> fields if only_fields and fields: raise Exception("Can't set both only_fields and fields") if only_fields: warnings.warn( "Defining `only_fields` is deprecated in favour of `fields`.", DeprecationWarning, stacklevel=2, ) fields = only_fields if fields and fields != ALL_FIELDS and not isinstance(fields, (list, tuple)): raise TypeError( 'The `fields` option must be a list or tuple or "__all__". ' "Got %s." % type(fields).__name__ ) # Alias exclude_fields -> exclude if exclude_fields and exclude: raise Exception("Can't set both exclude_fields and exclude") if exclude_fields: warnings.warn( "Defining `exclude_fields` is deprecated in favour of `exclude`.", DeprecationWarning, stacklevel=2, ) exclude = exclude_fields if exclude and not isinstance(exclude, (list, tuple)): raise TypeError( "The `exclude` option must be a list or tuple. Got %s." % type(exclude).__name__ ) if fields is None and exclude is None: warnings.warn( "Creating a DjangoObjectType without either the `fields` " "or the `exclude` option is deprecated. Add an explicit `fields " f"= '__all__'` option on DjangoObjectType {cls.__name__} to use all " "fields", DeprecationWarning, stacklevel=2, ) django_fields = yank_fields_from_attrs( construct_fields(model, registry, fields, exclude, convert_choices_to_enum), _as=graphene.Field, ) if use_connection is None and interfaces: use_connection = any( issubclass(interface, Node) for interface in interfaces ) if use_connection and not connection: # We create the connection automatically if not connection_class: connection_class = Connection connection = connection_class.create_type( "{}Connection".format(options.get("name") or cls.__name__), node=cls ) if connection is not None: assert issubclass( connection, Connection ), f"The connection must be a Connection. Received {connection.__name__}" if not _meta: _meta = DjangoObjectTypeOptions(cls) _meta.model = model _meta.registry = registry _meta.filter_fields = filter_fields _meta.filterset_class = filterset_class _meta.fields = django_fields _meta.connection = connection _meta.convert_choices_to_enum = convert_choices_to_enum super().__init_subclass_with_meta__( _meta=_meta, interfaces=interfaces, **options ) # Validate fields validate_fields(cls, model, _meta.fields, fields, exclude) if not skip_registry: registry.register(cls) def resolve_id(self, info): return self.pk @classmethod def is_type_of(cls, root, info): if isinstance(root, cls): return True if not is_valid_django_model(root.__class__): raise Exception(f'Received incompatible instance "{root}".') if cls._meta.model._meta.proxy: model = root._meta.model else: model = root._meta.model._meta.concrete_model return model == cls._meta.model @classmethod def get_queryset(cls, queryset, info): return queryset @classmethod def get_node(cls, info, id): queryset = cls.get_queryset(cls._meta.model.objects, info) try: return queryset.get(pk=id) except cls._meta.model.DoesNotExist: return None class ErrorType(ObjectType): field = graphene.String(required=True) messages = graphene.List(graphene.NonNull(graphene.String), required=True) @classmethod def from_errors(cls, errors): data = camelize(errors) if graphene_settings.CAMELCASE_ERRORS else errors return [cls(field=key, messages=value) for key, value in data.items()] django-graphene-3.2.3/graphene_django/utils/000077500000000000000000000000001476451357100210405ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/utils/__init__.py000066400000000000000000000006621476451357100231550ustar00rootroot00000000000000from .testing import GraphQLTestCase from .utils import ( DJANGO_FILTER_INSTALLED, bypass_get_queryset, camelize, get_model_fields, get_reverse_fields, is_valid_django_model, maybe_queryset, ) __all__ = [ "DJANGO_FILTER_INSTALLED", "get_reverse_fields", "maybe_queryset", "get_model_fields", "camelize", "is_valid_django_model", "GraphQLTestCase", "bypass_get_queryset", ] django-graphene-3.2.3/graphene_django/utils/str_converters.py000066400000000000000000000002061476451357100244720ustar00rootroot00000000000000import re from text_unidecode import unidecode def to_const(string): return re.sub(r"[\W|^]+", "_", unidecode(string)).upper() django-graphene-3.2.3/graphene_django/utils/testing.py000066400000000000000000000135061476451357100230740ustar00rootroot00000000000000import json import warnings from django.test import Client, TestCase, TransactionTestCase from graphene_django.settings import graphene_settings from graphene_django.utils.utils import _DJANGO_VERSION_AT_LEAST_4_2 DEFAULT_GRAPHQL_URL = "/graphql" def graphql_query( query, operation_name=None, input_data=None, variables=None, headers=None, client=None, graphql_url=None, ): """ Args: query (string) - GraphQL query to run operation_name (string) - If the query is a mutation or named query, you must supply the operation_name. For annon queries ("{ ... }"), should be None (default). input_data (dict) - If provided, the $input variable in GraphQL will be set to this value. If both ``input_data`` and ``variables``, are provided, the ``input`` field in the ``variables`` dict will be overwritten with this value. variables (dict) - If provided, the "variables" field in GraphQL will be set to this value. headers (dict) - If provided, the headers in POST request to GRAPHQL_URL will be set to this value. Keys should be prepended with "HTTP_" (e.g. to specify the "Authorization" HTTP header, use "HTTP_AUTHORIZATION" as the key). client (django.test.Client) - Test client. Defaults to django.test.Client. graphql_url (string) - URL to graphql endpoint. Defaults to "/graphql". Returns: Response object from client """ if client is None: client = Client() if not graphql_url: graphql_url = graphene_settings.TESTING_ENDPOINT body = {"query": query} if operation_name: body["operationName"] = operation_name if variables: body["variables"] = variables if input_data: if "variables" in body: body["variables"]["input"] = input_data else: body["variables"] = {"input": input_data} if headers: header_params = ( {"headers": headers} if _DJANGO_VERSION_AT_LEAST_4_2 else headers ) resp = client.post( graphql_url, json.dumps(body), content_type="application/json", **header_params, ) else: resp = client.post( graphql_url, json.dumps(body), content_type="application/json" ) return resp class GraphQLTestMixin: """ Based on: https://www.sam.today/blog/testing-graphql-with-graphene-django/ """ # URL to graphql endpoint GRAPHQL_URL = graphene_settings.TESTING_ENDPOINT def query( self, query, operation_name=None, input_data=None, variables=None, headers=None ): """ Args: query (string) - GraphQL query to run operation_name (string) - If the query is a mutation or named query, you must supply the operation_name. For annon queries ("{ ... }"), should be None (default). input_data (dict) - If provided, the $input variable in GraphQL will be set to this value. If both ``input_data`` and ``variables``, are provided, the ``input`` field in the ``variables`` dict will be overwritten with this value. variables (dict) - If provided, the "variables" field in GraphQL will be set to this value. headers (dict) - If provided, the headers in POST request to GRAPHQL_URL will be set to this value. Keys should be prepended with "HTTP_" (e.g. to specify the "Authorization" HTTP header, use "HTTP_AUTHORIZATION" as the key). Returns: Response object from client """ return graphql_query( query, operation_name=operation_name, input_data=input_data, variables=variables, headers=headers, client=self.client, graphql_url=self.GRAPHQL_URL, ) @property def _client(self): pass @_client.getter def _client(self): warnings.warn( "Using `_client` is deprecated in favour of `client`.", PendingDeprecationWarning, stacklevel=2, ) return self.client @_client.setter def _client(self, client): warnings.warn( "Using `_client` is deprecated in favour of `client`.", PendingDeprecationWarning, stacklevel=2, ) self.client = client def assertResponseNoErrors(self, resp, msg=None): """ Assert that the call went through correctly. 200 means the syntax is ok, if there are no `errors`, the call was fine. :resp HttpResponse: Response """ content = json.loads(resp.content) self.assertEqual(resp.status_code, 200, msg or content) self.assertNotIn("errors", list(content.keys()), msg or content) def assertResponseHasErrors(self, resp, msg=None): """ Assert that the call was failing. Take care: Even with errors, GraphQL returns status 200! :resp HttpResponse: Response """ content = json.loads(resp.content) self.assertIn("errors", list(content.keys()), msg or content) class GraphQLTestCase(GraphQLTestMixin, TestCase): pass class GraphQLTransactionTestCase(GraphQLTestMixin, TransactionTestCase): pass django-graphene-3.2.3/graphene_django/utils/tests/000077500000000000000000000000001476451357100222025ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/utils/tests/__init__.py000066400000000000000000000000001476451357100243010ustar00rootroot00000000000000django-graphene-3.2.3/graphene_django/utils/tests/test_str_converters.py000066400000000000000000000003701476451357100266750ustar00rootroot00000000000000from ..str_converters import to_const def test_to_const(): assert to_const('snakes $1. on a "#plane') == "SNAKES_1_ON_A_PLANE" def test_to_const_unicode(): assert to_const("Skoða þetta unicode stöff") == "SKODA_THETTA_UNICODE_STOFF" django-graphene-3.2.3/graphene_django/utils/tests/test_testing.py000066400000000000000000000023531476451357100252730ustar00rootroot00000000000000import pytest from django.test import Client from ...settings import graphene_settings from ...tests.test_types import with_local_registry from .. import GraphQLTestCase @with_local_registry def test_graphql_test_case_deprecated_client_getter(): """ `GraphQLTestCase._client`' getter should raise pending deprecation warning. """ class TestClass(GraphQLTestCase): GRAPHQL_SCHEMA = True def runTest(self): pass tc = TestClass() tc._pre_setup() tc.setUpClass() with pytest.warns(PendingDeprecationWarning): tc._client # noqa: B018 @with_local_registry def test_graphql_test_case_deprecated_client_setter(): """ `GraphQLTestCase._client`' setter should raise pending deprecation warning. """ class TestClass(GraphQLTestCase): GRAPHQL_SCHEMA = True def runTest(self): pass tc = TestClass() tc._pre_setup() tc.setUpClass() with pytest.warns(PendingDeprecationWarning): tc._client = Client() def test_graphql_test_case_imports_endpoint(): """ GraphQLTestCase class should import the default endpoint from settings file """ assert GraphQLTestCase.GRAPHQL_URL == graphene_settings.TESTING_ENDPOINT django-graphene-3.2.3/graphene_django/utils/utils.py000066400000000000000000000075241476451357100225620ustar00rootroot00000000000000import inspect import django from django.db import connection, models, transaction from django.db.models.manager import Manager from django.utils.encoding import force_str from django.utils.functional import Promise from graphene.utils.str_converters import to_camel_case try: import django_filters # noqa DJANGO_FILTER_INSTALLED = True except ImportError: DJANGO_FILTER_INSTALLED = False def isiterable(value): try: iter(value) except TypeError: return False return True def _camelize_django_str(s): if isinstance(s, Promise): s = force_str(s) return to_camel_case(s) if isinstance(s, str) else s def camelize(data): if isinstance(data, dict): return {_camelize_django_str(k): camelize(v) for k, v in data.items()} if isiterable(data) and not isinstance(data, (str, Promise)): return [camelize(d) for d in data] return data def _get_model_ancestry(model): model_ancestry = [model] for base in model.__bases__: if is_valid_django_model(base) and getattr(base, "_meta", False): model_ancestry.append(base) return model_ancestry def get_reverse_fields(model, local_field_names): """ Searches through the model's ancestry and gets reverse relationships the models Yields a tuple of (field.name, field) """ model_ancestry = _get_model_ancestry(model) for _model in model_ancestry: for name, attr in _model.__dict__.items(): # Don't duplicate any local fields if name in local_field_names: continue # "rel" for FK and M2M relations and "related" for O2O Relations related = getattr(attr, "rel", None) or getattr(attr, "related", None) if isinstance(related, models.ManyToOneRel): yield (name, related) elif isinstance(related, models.ManyToManyRel) and not related.symmetrical: yield (name, related) def get_local_fields(model): """ Searches through the model's ancestry and gets the fields on the models Returns a dict of {field.name: field} """ model_ancestry = _get_model_ancestry(model) local_fields_dict = {} for _model in model_ancestry: for field in sorted( list(_model._meta.fields) + list(_model._meta.local_many_to_many) ): if field.name not in local_fields_dict: local_fields_dict[field.name] = field return list(local_fields_dict.items()) def maybe_queryset(value): if isinstance(value, Manager): value = value.get_queryset() return value def get_model_fields(model): """ Gets all the fields and relationships on the Django model and its ancestry. Prioritizes local fields and relationships over the reverse relationships of the same name Returns a tuple of (field.name, field) """ local_fields = get_local_fields(model) local_field_names = {field[0] for field in local_fields} reverse_fields = get_reverse_fields(model, local_field_names) all_fields = local_fields + list(reverse_fields) return all_fields def is_valid_django_model(model): return inspect.isclass(model) and issubclass(model, models.Model) def import_single_dispatch(): from functools import singledispatch return singledispatch def set_rollback(): atomic_requests = connection.settings_dict.get("ATOMIC_REQUESTS", False) if atomic_requests and connection.in_atomic_block: transaction.set_rollback(True) def bypass_get_queryset(resolver): """ Adds a bypass_get_queryset attribute to the resolver, which is used to bypass any custom get_queryset method of the DjangoObjectType. """ resolver._bypass_get_queryset = True return resolver _DJANGO_VERSION_AT_LEAST_4_2 = django.VERSION[0] > 4 or ( django.VERSION[0] >= 4 and django.VERSION[1] >= 2 ) django-graphene-3.2.3/graphene_django/views.py000066400000000000000000000362721476451357100214210ustar00rootroot00000000000000import inspect import json import re from django.db import connection, transaction from django.http import HttpResponse, HttpResponseNotAllowed from django.http.response import HttpResponseBadRequest from django.shortcuts import render from django.utils.decorators import method_decorator from django.views.decorators.csrf import ensure_csrf_cookie from django.views.generic import View from graphql import ( ExecutionResult, OperationType, execute, get_operation_ast, parse, validate_schema, ) from graphql.error import GraphQLError from graphql.execution.middleware import MiddlewareManager from graphql.validation import validate from graphene import Schema from graphene_django.constants import MUTATION_ERRORS_FLAG from graphene_django.utils.utils import set_rollback from .settings import graphene_settings class HttpError(Exception): def __init__(self, response, message=None, *args, **kwargs): self.response = response self.message = message = message or response.content.decode() super().__init__(message, *args, **kwargs) def get_accepted_content_types(request): def qualify(x): parts = x.split(";", 1) if len(parts) == 2: match = re.match(r"(^|;)q=(0(\.\d{,3})?|1(\.0{,3})?)(;|$)", parts[1]) if match: return parts[0].strip(), float(match.group(2)) return parts[0].strip(), 1 raw_content_types = request.META.get("HTTP_ACCEPT", "*/*").split(",") qualified_content_types = map(qualify, raw_content_types) return [ x[0] for x in sorted(qualified_content_types, key=lambda x: x[1], reverse=True) ] def instantiate_middleware(middlewares): for middleware in middlewares: if inspect.isclass(middleware): yield middleware() continue yield middleware class GraphQLView(View): graphiql_template = "graphene/graphiql.html" # Polyfill for window.fetch. whatwg_fetch_version = "3.6.2" whatwg_fetch_sri = "sha256-+pQdxwAcHJdQ3e/9S4RK6g8ZkwdMgFQuHvLuN5uyk5c=" # React and ReactDOM. react_version = "17.0.2" react_sri = "sha256-Ipu/TQ50iCCVZBUsZyNJfxrDk0E2yhaEIz0vqI+kFG8=" react_dom_sri = "sha256-nbMykgB6tsOFJ7OdVmPpdqMFVk4ZsqWocT6issAPUF0=" # The GraphiQL React app. graphiql_version = "2.4.7" graphiql_sri = "sha256-n/LKaELupC1H/PU6joz+ybeRJHT2xCdekEt6OYMOOZU=" graphiql_css_sri = "sha256-OsbM+LQHcnFHi0iH7AUKueZvDcEBoy/z4hJ7jx1cpsM=" # The websocket transport library for subscriptions. subscriptions_transport_ws_version = "5.13.1" subscriptions_transport_ws_sri = ( "sha256-EZhvg6ANJrBsgLvLAa0uuHNLepLJVCFYS+xlb5U/bqw=" ) graphiql_plugin_explorer_version = "0.1.15" graphiql_plugin_explorer_sri = "sha256-3hUuhBXdXlfCj6RTeEkJFtEh/kUG+TCDASFpFPLrzvE=" graphiql_plugin_explorer_css_sri = ( "sha256-fA0LPUlukMNR6L4SPSeFqDTYav8QdWjQ2nr559Zln1U=" ) schema = None graphiql = False middleware = None root_value = None pretty = False batch = False subscription_path = None execution_context_class = None validation_rules = None def __init__( self, schema=None, middleware=None, root_value=None, graphiql=False, pretty=False, batch=False, subscription_path=None, execution_context_class=None, validation_rules=None, ): if not schema: schema = graphene_settings.SCHEMA if middleware is None: middleware = graphene_settings.MIDDLEWARE self.schema = schema or self.schema if middleware is not None: if isinstance(middleware, MiddlewareManager): self.middleware = middleware else: self.middleware = list(instantiate_middleware(middleware)) self.root_value = root_value self.pretty = pretty or self.pretty self.graphiql = graphiql or self.graphiql self.batch = batch or self.batch self.execution_context_class = ( execution_context_class or self.execution_context_class ) if subscription_path is None: self.subscription_path = graphene_settings.SUBSCRIPTION_PATH assert isinstance( self.schema, Schema ), "A Schema is required to be provided to GraphQLView." assert not all((graphiql, batch)), "Use either graphiql or batch processing" self.validation_rules = validation_rules or self.validation_rules # noinspection PyUnusedLocal def get_root_value(self, request): return self.root_value def get_middleware(self, request): return self.middleware def get_context(self, request): return request @method_decorator(ensure_csrf_cookie) def dispatch(self, request, *args, **kwargs): try: if request.method.lower() not in ("get", "post"): raise HttpError( HttpResponseNotAllowed( ["GET", "POST"], "GraphQL only supports GET and POST requests." ) ) data = self.parse_body(request) show_graphiql = self.graphiql and self.can_display_graphiql(request, data) if show_graphiql: return self.render_graphiql( request, # Dependency parameters. whatwg_fetch_version=self.whatwg_fetch_version, whatwg_fetch_sri=self.whatwg_fetch_sri, react_version=self.react_version, react_sri=self.react_sri, react_dom_sri=self.react_dom_sri, graphiql_version=self.graphiql_version, graphiql_sri=self.graphiql_sri, graphiql_css_sri=self.graphiql_css_sri, subscriptions_transport_ws_version=self.subscriptions_transport_ws_version, subscriptions_transport_ws_sri=self.subscriptions_transport_ws_sri, graphiql_plugin_explorer_version=self.graphiql_plugin_explorer_version, graphiql_plugin_explorer_sri=self.graphiql_plugin_explorer_sri, graphiql_plugin_explorer_css_sri=self.graphiql_plugin_explorer_css_sri, # The SUBSCRIPTION_PATH setting. subscription_path=self.subscription_path, # GraphiQL headers tab, graphiql_header_editor_enabled=graphene_settings.GRAPHIQL_HEADER_EDITOR_ENABLED, graphiql_should_persist_headers=graphene_settings.GRAPHIQL_SHOULD_PERSIST_HEADERS, graphiql_input_value_deprecation=graphene_settings.GRAPHIQL_INPUT_VALUE_DEPRECATION, ) if self.batch: responses = [self.get_response(request, entry) for entry in data] result = "[{}]".format( ",".join([response[0] for response in responses]) ) status_code = ( responses and max(responses, key=lambda response: response[1])[1] or 200 ) else: result, status_code = self.get_response(request, data, show_graphiql) return HttpResponse( status=status_code, content=result, content_type="application/json" ) except HttpError as e: response = e.response response["Content-Type"] = "application/json" response.content = self.json_encode( request, {"errors": [self.format_error(e)]} ) return response def get_response(self, request, data, show_graphiql=False): query, variables, operation_name, id = self.get_graphql_params(request, data) execution_result = self.execute_graphql_request( request, data, query, variables, operation_name, show_graphiql ) if getattr(request, MUTATION_ERRORS_FLAG, False) is True: set_rollback() status_code = 200 if execution_result: response = {} if execution_result.errors: set_rollback() response["errors"] = [ self.format_error(e) for e in execution_result.errors ] if execution_result.errors and any( not getattr(e, "path", None) for e in execution_result.errors ): status_code = 400 else: response["data"] = execution_result.data if self.batch: response["id"] = id response["status"] = status_code result = self.json_encode(request, response, pretty=show_graphiql) else: result = None return result, status_code def render_graphiql(self, request, **data): return render(request, self.graphiql_template, data) def json_encode(self, request, d, pretty=False): if not (self.pretty or pretty) and not request.GET.get("pretty"): return json.dumps(d, separators=(",", ":")) return json.dumps(d, sort_keys=True, indent=2, separators=(",", ": ")) def parse_body(self, request): content_type = self.get_content_type(request) if content_type == "application/graphql": return {"query": request.body.decode()} elif content_type == "application/json": # noinspection PyBroadException try: body = request.body.decode("utf-8") except Exception as e: raise HttpError(HttpResponseBadRequest(str(e))) try: request_json = json.loads(body) if self.batch: assert isinstance(request_json, list), ( "Batch requests should receive a list, but received {}." ).format(repr(request_json)) assert ( len(request_json) > 0 ), "Received an empty list in the batch request." else: assert isinstance( request_json, dict ), "The received data is not a valid JSON query." return request_json except AssertionError as e: raise HttpError(HttpResponseBadRequest(str(e))) except (TypeError, ValueError): raise HttpError(HttpResponseBadRequest("POST body sent invalid JSON.")) elif content_type in [ "application/x-www-form-urlencoded", "multipart/form-data", ]: return request.POST return {} def execute_graphql_request( self, request, data, query, variables, operation_name, show_graphiql=False ): if not query: if show_graphiql: return None raise HttpError(HttpResponseBadRequest("Must provide query string.")) schema = self.schema.graphql_schema schema_validation_errors = validate_schema(schema) if schema_validation_errors: return ExecutionResult(data=None, errors=schema_validation_errors) try: document = parse(query) except Exception as e: return ExecutionResult(errors=[e]) operation_ast = get_operation_ast(document, operation_name) if ( request.method.lower() == "get" and operation_ast is not None and operation_ast.operation != OperationType.QUERY ): if show_graphiql: return None raise HttpError( HttpResponseNotAllowed( ["POST"], "Can only perform a {} operation from a POST request.".format( operation_ast.operation.value ), ) ) validation_errors = validate( schema, document, self.validation_rules, graphene_settings.MAX_VALIDATION_ERRORS, ) if validation_errors: return ExecutionResult(data=None, errors=validation_errors) try: execute_options = { "root_value": self.get_root_value(request), "context_value": self.get_context(request), "variable_values": variables, "operation_name": operation_name, "middleware": self.get_middleware(request), } if self.execution_context_class: execute_options[ "execution_context_class" ] = self.execution_context_class if ( operation_ast is not None and operation_ast.operation == OperationType.MUTATION and ( graphene_settings.ATOMIC_MUTATIONS is True or connection.settings_dict.get("ATOMIC_MUTATIONS", False) is True ) ): with transaction.atomic(): result = execute(schema, document, **execute_options) if getattr(request, MUTATION_ERRORS_FLAG, False) is True: transaction.set_rollback(True) return result return execute(schema, document, **execute_options) except Exception as e: return ExecutionResult(errors=[e]) @classmethod def can_display_graphiql(cls, request, data): raw = "raw" in request.GET or "raw" in data return not raw and cls.request_wants_html(request) @classmethod def request_wants_html(cls, request): accepted = get_accepted_content_types(request) accepted_length = len(accepted) # the list will be ordered in preferred first - so we have to make # sure the most preferred gets the highest number html_priority = ( accepted_length - accepted.index("text/html") if "text/html" in accepted else 0 ) json_priority = ( accepted_length - accepted.index("application/json") if "application/json" in accepted else 0 ) return html_priority > json_priority @staticmethod def get_graphql_params(request, data): query = request.GET.get("query") or data.get("query") variables = request.GET.get("variables") or data.get("variables") id = request.GET.get("id") or data.get("id") if variables and isinstance(variables, str): try: variables = json.loads(variables) except Exception: raise HttpError(HttpResponseBadRequest("Variables are invalid JSON.")) operation_name = request.GET.get("operationName") or data.get("operationName") if operation_name == "null": operation_name = None return query, variables, operation_name, id @staticmethod def format_error(error): if isinstance(error, GraphQLError): return error.formatted return {"message": str(error)} @staticmethod def get_content_type(request): meta = request.META content_type = meta.get("CONTENT_TYPE", meta.get("HTTP_CONTENT_TYPE", "")) return content_type.split(";", 1)[0].lower() django-graphene-3.2.3/setup.cfg000066400000000000000000000006071476451357100164110ustar00rootroot00000000000000[aliases] test=pytest [bdist_wheel] universal=1 [coverage:run] omit = */tests/* [tool:pytest] DJANGO_SETTINGS_MODULE = examples.django_test_settings addopts = --random-order filterwarnings = error # we can't do anything about the DeprecationWarning about typing.ByteString in graphql default:'typing\.ByteString' is deprecated:DeprecationWarning:graphql\.pyutils\.is_iterable django-graphene-3.2.3/setup.py000066400000000000000000000044131476451357100163010ustar00rootroot00000000000000import ast import re from setuptools import find_packages, setup _version_re = re.compile(r"__version__\s+=\s+(.*)") with open("graphene_django/__init__.py", "rb") as f: version = str( ast.literal_eval(_version_re.search(f.read().decode("utf-8")).group(1)) ) rest_framework_require = ["djangorestframework>=3.6.3"] tests_require = [ "pytest>=7.3.1", "pytest-cov", "pytest-random-order", "coveralls", "mock", "pytz", "django-filter>=22.1", "pytest-django>=4.5.2", ] + rest_framework_require dev_requires = [ "ruff==0.1.2", "pre-commit", ] + tests_require setup( name="graphene-django", version=version, description="Graphene Django integration", long_description=open("README.md").read(), long_description_content_type="text/markdown", url="https://github.com/graphql-python/graphene-django", author="Syrus Akbary", author_email="me@syrusakbary.com", license="MIT", classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Topic :: Software Development :: Libraries", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: PyPy", "Framework :: Django", "Framework :: Django :: 3.2", "Framework :: Django :: 4.1", "Framework :: Django :: 4.2", ], keywords="api graphql protocol rest relay graphene", packages=find_packages(exclude=["tests", "examples", "examples.*"]), install_requires=[ "graphene>=3.0,<4", "graphql-core>=3.1.0,<4", "graphql-relay>=3.1.1,<4", "Django>=3.2", "promise>=2.1", "text-unidecode", ], setup_requires=["pytest-runner"], tests_require=tests_require, rest_framework_require=rest_framework_require, extras_require={ "test": tests_require, "rest_framework": rest_framework_require, "dev": dev_requires, }, include_package_data=True, zip_safe=False, platforms="any", ) django-graphene-3.2.3/tox.ini000066400000000000000000000016131476451357100161010ustar00rootroot00000000000000[tox] envlist = py{38,39,310}-django32 py{38,39}-django42 py{310,311,312}-django{42,50,51,main} pre-commit [gh-actions] python = 3.8: py38 3.9: py39 3.10: py310 3.11: py311 3.12: py312 [gh-actions:env] DJANGO = 3.2: django32 4.2: django42 5.0: django50 5.1: django51 main: djangomain [testenv] passenv = * usedevelop = True setenv = DJANGO_SETTINGS_MODULE=examples.django_test_settings PYTHONPATH=. deps = -e.[test] psycopg2-binary django32: Django>=3.2,<4.0 django42: Django>=4.2,<4.3 django50: Django>=5.0,<5.1 django51: Django>=5.1,<5.2 djangomain: https://github.com/django/django/archive/main.zip commands = {posargs:pytest --cov=graphene_django graphene_django examples} [testenv:pre-commit] skip_install = true deps = pre-commit commands = pre-commit run {posargs:--all-files --show-diff-on-failure}