From 367c077a49699ce822e37ce43432e47ce1d10260 Mon Sep 17 00:00:00 2001 From: sierreis <48896364+sierreis@users.noreply.github.com> Date: Mon, 25 Mar 2019 12:45:43 -0400 Subject: [PATCH 01/26] Add static files to MANIFEST.in At the moment, static files are not included in the package data when installing using setuptools. This is necessary for the GraphiQL view. --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 3c3d4f9..4677330 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,3 @@ include README.md LICENSE recursive-include graphene_django/templates * +recursive-include graphene_django/static * From 547a4cb5767d97ba5d2c364bd525c14330f6330d Mon Sep 17 00:00:00 2001 From: Ronny Vedrilla Date: Wed, 27 Mar 2019 16:30:35 +0100 Subject: [PATCH 02/26] Missing LOC in django model form documentation (fixes #602) --- docs/form-mutations.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/form-mutations.rst b/docs/form-mutations.rst index bbaadb1..85b89e8 100644 --- a/docs/form-mutations.rst +++ b/docs/form-mutations.rst @@ -43,6 +43,8 @@ DjangoModelFormMutation model = Pet class PetMutation(DjangoModelFormMutation): + pet = Field(PetType) + class Meta: form_class = PetForm From b491878c27eb117ac72958c72333b7a9b10e6d16 Mon Sep 17 00:00:00 2001 From: Ronny Vedrilla Date: Fri, 29 Mar 2019 11:51:40 +0100 Subject: [PATCH 03/26] * Added test class for django api unittests and documentation how to use it --- docs/index.rst | 1 + docs/testing.rst | 60 ++++++++++++++++++++++++++++ graphene_django/tests/base_test.py | 64 ++++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 docs/testing.rst create mode 100644 graphene_django/tests/base_test.py diff --git a/docs/index.rst b/docs/index.rst index 7c64ae7..9469c29 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,3 +14,4 @@ Contents: rest-framework form-mutations introspection + testing diff --git a/docs/testing.rst b/docs/testing.rst new file mode 100644 index 0000000..a4d5518 --- /dev/null +++ b/docs/testing.rst @@ -0,0 +1,60 @@ +Testing API calls with django +============================= + +If you want to unittest your API calls derive your test case from the class `GraphQLTestCase`. + +Usage: + +.. code:: python + + import json + + from graphene_django.tests.base_test import GraphQLTestCase + from my_project.config.schema import schema + + class MyFancyTestCase(GraphQLTestCase): + # Here you need to inject your test case's schema + GRAPHQL_SCHEMA = schema + + def test_some_query(self): + response = self.query( + ''' + query { + myModel { + id + name + } + } + ''', + op_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_some_mutation(self): + response = self.query( + ''' + mutation myMutation($input: MyMutationInput!) { + myMutation(input: $input) { + my-model { + id + name + } + } + } + ''', + op_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 + ... diff --git a/graphene_django/tests/base_test.py b/graphene_django/tests/base_test.py new file mode 100644 index 0000000..8ec2fae --- /dev/null +++ b/graphene_django/tests/base_test.py @@ -0,0 +1,64 @@ +import json + +from django.http import HttpResponse +from django.test import Client +from django.test import TestCase + + +class GraphQLTestCase(TestCase): + """ + Based on: https://www.sam.today/blog/testing-graphql-with-graphene-django/ + """ + + # URL to graphql endpoint + GRAPHQL_URL = '/graphql/' + # Here you need to set your graphql schema for the tests + GRAPHQL_SCHEMA = None + + @classmethod + def setUpClass(cls): + super().setUpClass() + + if not cls.GRAPHQL_SCHEMA: + raise AttributeError('Variable GRAPHQL_SCHEMA not defined in GraphQLTestCase.') + + cls._client = Client(cls.GRAPHQL_SCHEMA) + + def query(self, query: str, op_name: str = None, input_data: dict = None): + """ + Args: + query (string) - GraphQL query to run + op_name (string) - If the query is a mutation or named query, you must + supply the op_name. For annon queries ("{ ... }"), + should be None (default). + input_data (dict) - If provided, the $input variable in GraphQL will be set + to this value + + Returns: + Response object from client + """ + body = {'query': query} + if op_name: + body['operation_name'] = op_name + if input_data: + body['variables'] = {'input': input_data} + + resp = self._client.post(self.GRAPHQL_URL, json.dumps(body), + content_type='application/json') + return resp + + def assertResponseNoErrors(self, resp: HttpResponse): + """ + Assert that the call went through correctly. 200 means the syntax is ok, if there are no `errors`, + the call was fine. + """ + content = json.loads(resp.content) + self.assertEqual(resp.status_code, 200) + self.assertNotIn('errors', list(content.keys())) + + def assertResponseHasErrors(self, resp: HttpResponse): + """ + Assert that the call was failing. Take care: Even with errors, GraphQL returns status 200! + """ + content = json.loads(resp.content) + self.assertIn('errors', list(content.keys())) From 3c11a980febe976669dda08e4ef00c8152530f8e Mon Sep 17 00:00:00 2001 From: Ronny Vedrilla Date: Fri, 29 Mar 2019 12:53:18 +0100 Subject: [PATCH 04/26] Python 2.7 syntax compat --- graphene_django/tests/base_test.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/graphene_django/tests/base_test.py b/graphene_django/tests/base_test.py index 8ec2fae..471dffe 100644 --- a/graphene_django/tests/base_test.py +++ b/graphene_django/tests/base_test.py @@ -17,14 +17,14 @@ class GraphQLTestCase(TestCase): @classmethod def setUpClass(cls): - super().setUpClass() + super(GraphQLTestCase, cls).setUpClass() if not cls.GRAPHQL_SCHEMA: raise AttributeError('Variable GRAPHQL_SCHEMA not defined in GraphQLTestCase.') cls._client = Client(cls.GRAPHQL_SCHEMA) - def query(self, query: str, op_name: str = None, input_data: dict = None): + def query(self, query, op_name=None, input_data=None): """ Args: query (string) - GraphQL query to run @@ -47,18 +47,20 @@ class GraphQLTestCase(TestCase): content_type='application/json') return resp - def assertResponseNoErrors(self, resp: HttpResponse): + def assertResponseNoErrors(self, resp): """ 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) self.assertNotIn('errors', list(content.keys())) - def assertResponseHasErrors(self, resp: HttpResponse): + def assertResponseHasErrors(self, resp): """ 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())) From 8beadc759f1eb98174a65dd206d25f9835596827 Mon Sep 17 00:00:00 2001 From: Alexandre Kirszenberg Date: Sat, 30 Mar 2019 19:38:20 +0100 Subject: [PATCH 05/26] Correctly propagate NonNull to inner connection type --- graphene_django/fields.py | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 1ecce45..82c9c66 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -1,6 +1,7 @@ from functools import partial from django.db.models.query import QuerySet +from graphene import NonNull from promise import Promise @@ -45,17 +46,31 @@ class DjangoConnectionField(ConnectionField): 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__ ) - return _type._meta.connection + 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.type._meta.node + return self.connection_type._meta.node @property def model(self): @@ -103,15 +118,15 @@ class DjangoConnectionField(ConnectionField): @classmethod def connection_resolver( - cls, - resolver, - connection, - default_manager, - max_limit, - enforce_first_or_last, - root, - info, - **args + cls, + resolver, + connection, + default_manager, + max_limit, + enforce_first_or_last, + root, + info, + **args ): first = args.get("first") last = args.get("last") @@ -146,7 +161,7 @@ class DjangoConnectionField(ConnectionField): return partial( self.connection_resolver, parent_resolver, - self.type, + self.connection_type, self.get_manager(), self.max_limit, self.enforce_first_or_last, From 29b8ea8398217c0d1c62ad0fbd4c3e5c225e67a9 Mon Sep 17 00:00:00 2001 From: Ronny Vedrilla Date: Fri, 5 Apr 2019 14:27:53 +0200 Subject: [PATCH 06/26] Bugfix: FormMutation was always causing boolean fields to be required --- graphene_django/forms/converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/forms/converter.py b/graphene_django/forms/converter.py index 87180b2..8916456 100644 --- a/graphene_django/forms/converter.py +++ b/graphene_django/forms/converter.py @@ -43,7 +43,7 @@ def convert_form_field_to_int(field): @convert_form_field.register(forms.BooleanField) def convert_form_field_to_boolean(field): - return Boolean(description=field.help_text, required=True) + return Boolean(description=field.help_text, required=field.required) @convert_form_field.register(forms.NullBooleanField) From 6acd917cf7076397009d0ba77901f4c1c8e190fe Mon Sep 17 00:00:00 2001 From: David Sanders Date: Mon, 15 Apr 2019 05:53:30 -0700 Subject: [PATCH 07/26] Drop old Django compatibility code --- graphene_django/utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/graphene_django/utils.py b/graphene_django/utils.py index 560f604..532be06 100644 --- a/graphene_django/utils.py +++ b/graphene_django/utils.py @@ -25,8 +25,7 @@ def get_reverse_fields(model, local_field_names): if name in local_field_names: continue - # Django =>1.9 uses 'rel', django <1.9 uses 'related' - related = getattr(attr, "rel", None) or getattr(attr, "related", None) + related = getattr(attr, "rel", None) if isinstance(related, models.ManyToOneRel): yield (name, related) elif isinstance(related, models.ManyToManyRel) and not related.symmetrical: From 2ae897187cec9119de3753bc646e806b69487188 Mon Sep 17 00:00:00 2001 From: Paul Hallett Date: Fri, 26 Apr 2019 13:14:28 +0100 Subject: [PATCH 08/26] Add Makefile and better CONTRIBUTING.md --- CONTRIBUTING.md | 32 ++++++++++++++++++++++++++++++++ Makefile | 5 +++++ README.md | 12 +----------- README.rst | 12 +----------- 4 files changed, 39 insertions(+), 22 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 Makefile diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9731c03 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,32 @@ +# 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 +``` \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5c174ac --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +dev-setup: + pip install -e ".[test]" + +tests: + py.test graphene_django --cov=graphene_django -vv \ No newline at end of file diff --git a/README.md b/README.md index ef3f40c..9b8916a 100644 --- a/README.md +++ b/README.md @@ -96,17 +96,7 @@ To learn more check out the following [examples](examples/): ## Contributing -After cloning this repo, ensure dependencies are installed by running: - -```sh -pip install -e ".[test]" -``` - -After developing, the full test suite can be evaluated by running: - -```sh -py.test graphene_django --cov=graphene_django # Use -v -s for verbose mode -``` +See [CONTRIBUTING.md](contributing.md) ### Documentation diff --git a/README.rst b/README.rst index a96e60f..e884a40 100644 --- a/README.rst +++ b/README.rst @@ -105,17 +105,7 @@ To learn more check out the following `examples `__: Contributing ------------ -After cloning this repo, ensure dependencies are installed by running: - -.. code:: sh - - pip install -e ".[test]" - -After developing, the full test suite can be evaluated by running: - -.. code:: sh - - py.test graphene_django --cov=graphene_django # Use -v -s for verbose mode +See `CONTRIBUTING.md `__. Documentation ~~~~~~~~~~~~~ From bba8377a8209235fadb07848e11429c2d18aeff4 Mon Sep 17 00:00:00 2001 From: Paul Hallett Date: Fri, 26 Apr 2019 14:08:44 +0100 Subject: [PATCH 09/26] Move documentation to CONTRIBUTING.md --- CONTRIBUTING.md | 17 +++++++++++++++++ README.md | 20 +------------------- README.rst | 19 ------------------- 3 files changed, 18 insertions(+), 38 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9731c03..4a650d6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,4 +29,21 @@ After developing, the full test suite can be evaluated by running: ```sh make tests +``` + +## 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 ``` \ No newline at end of file diff --git a/README.md b/README.md index 9b8916a..fffa1d3 100644 --- a/README.md +++ b/README.md @@ -96,22 +96,4 @@ To learn more check out the following [examples](examples/): ## Contributing -See [CONTRIBUTING.md](contributing.md) - - -### 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 -``` +See [CONTRIBUTING.md](contributing.md) \ No newline at end of file diff --git a/README.rst b/README.rst index e884a40..2e0593d 100644 --- a/README.rst +++ b/README.rst @@ -107,25 +107,6 @@ Contributing See `CONTRIBUTING.md `__. -Documentation -~~~~~~~~~~~~~ - -The `documentation `__ is generated using the excellent -`Sphinx `__ and a custom theme. - -The documentation dependencies are installed by running: - -.. code:: sh - - cd docs - pip install -r requirements.txt - -Then to produce a HTML version of the documentation: - -.. code:: sh - - make html - .. |Graphene Logo| image:: http://graphene-python.org/favicon.png .. |Build Status| image:: https://travis-ci.org/graphql-python/graphene-django.svg?branch=master :target: https://travis-ci.org/graphql-python/graphene-django From d720b47c8da0eb4ca1e1ca8ba9cda72b56750295 Mon Sep 17 00:00:00 2001 From: Eran Kampf <205185+ekampf@users.noreply.github.com> Date: Tue, 30 Apr 2019 09:55:28 -0700 Subject: [PATCH 10/26] Test docs build --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index 2e0593d..8afb5ce 100644 --- a/README.rst +++ b/README.rst @@ -114,3 +114,4 @@ See `CONTRIBUTING.md `__. :target: https://badge.fury.io/py/graphene-django .. |Coverage Status| image:: https://coveralls.io/repos/graphql-python/graphene-django/badge.svg?branch=master&service=github :target: https://coveralls.io/github/graphql-python/graphene-django?branch=master + From 05c89c19fb48c26fd96a4b3d590aeb98ad11dfc0 Mon Sep 17 00:00:00 2001 From: Eran Kampf <205185+ekampf@users.noreply.github.com> Date: Tue, 30 Apr 2019 09:57:17 -0700 Subject: [PATCH 11/26] Test docs integration webhook --- README.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/README.rst b/README.rst index 8afb5ce..2e0593d 100644 --- a/README.rst +++ b/README.rst @@ -114,4 +114,3 @@ See `CONTRIBUTING.md `__. :target: https://badge.fury.io/py/graphene-django .. |Coverage Status| image:: https://coveralls.io/repos/graphql-python/graphene-django/badge.svg?branch=master&service=github :target: https://coveralls.io/github/graphql-python/graphene-django?branch=master - From b49d315a39c3a771a98ab35b6c35dbe56cb8ab3a Mon Sep 17 00:00:00 2001 From: Alexandre Kirszenberg Date: Wed, 1 May 2019 15:49:54 +0200 Subject: [PATCH 12/26] 4 spaces --- graphene_django/fields.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 82c9c66..35bd8a4 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -118,15 +118,15 @@ class DjangoConnectionField(ConnectionField): @classmethod def connection_resolver( - cls, - resolver, - connection, - default_manager, - max_limit, - enforce_first_or_last, - root, - info, - **args + cls, + resolver, + connection, + default_manager, + max_limit, + enforce_first_or_last, + root, + info, + **args ): first = args.get("first") last = args.get("last") From e6ad5887caebf818b0c61d09924c51a9f0f9b406 Mon Sep 17 00:00:00 2001 From: Paul Hallett Date: Tue, 30 Apr 2019 10:02:23 +0100 Subject: [PATCH 13/26] Introduce Black formatting, additional tests --- .travis.yml | 14 +++++++-- CONTRIBUTING.md | 13 +++++++++ Makefile | 10 +++++-- README.md | 2 +- graphene_django/compat.py | 8 +++-- graphene_django/debug/sql/types.py | 29 +++++-------------- graphene_django/debug/types.py | 5 +--- graphene_django/filter/filterset.py | 4 +-- graphene_django/forms/tests/test_mutation.py | 9 +++--- .../management/commands/graphql_schema.py | 2 +- graphene_django/tests/base_test.py | 21 ++++++++------ graphene_django/tests/test_command.py | 14 +++++---- graphene_django/tests/test_converter.py | 6 ++-- graphene_django/views.py | 3 +- setup.py | 8 ++++- 15 files changed, 86 insertions(+), 62 deletions(-) diff --git a/.travis.yml b/.travis.yml index a8375ee..07ee59f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,19 +1,21 @@ language: python -sudo: false +sudo: required +dist: xenial python: - 2.7 - 3.4 - 3.5 - 3.6 +- 3.7 install: - | if [ "$TEST_TYPE" = build ]; then pip install -e .[test] - pip install psycopg2 # Required for Django postgres fields testing + pip install psycopg2==2.8.2 # Required for Django postgres fields testing pip install django==$DJANGO_VERSION python setup.py develop elif [ "$TEST_TYPE" = lint ]; then - pip install flake8 + pip install flake8==3.7.7 fi script: - | @@ -45,10 +47,16 @@ matrix: env: TEST_TYPE=build DJANGO_VERSION=2.1 - python: '3.6' env: TEST_TYPE=build DJANGO_VERSION=2.1 + - python: '3.6' + env: TEST_TYPE=build DJANGO_VERSION=2.2 + - python: '3.7' + env: TEST_TYPE=build DJANGO_VERSION=2.2 - python: '2.7' env: TEST_TYPE=lint - python: '3.6' env: TEST_TYPE=lint + - python: '3.7' + env: TEST_TYPE=lint deploy: provider: pypi user: syrusakbary diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4a650d6..f9428e9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,6 +31,19 @@ After developing, the full test suite can be evaluated by running: make tests ``` +## Opening Pull Requests + +Please fork the project and open a pull request against the master 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. diff --git a/Makefile b/Makefile index 5c174ac..061ad4e 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,11 @@ dev-setup: - pip install -e ".[test]" + pip install -e ".[dev]" tests: - py.test graphene_django --cov=graphene_django -vv \ No newline at end of file + py.test graphene_django --cov=graphene_django -vv + +format: + black graphene_django + +lint: + flake8 graphene_django diff --git a/README.md b/README.md index fffa1d3..d2fe4b6 100644 --- a/README.md +++ b/README.md @@ -96,4 +96,4 @@ To learn more check out the following [examples](examples/): ## Contributing -See [CONTRIBUTING.md](contributing.md) \ No newline at end of file +See [CONTRIBUTING.md](CONTRIBUTING.md) \ No newline at end of file diff --git a/graphene_django/compat.py b/graphene_django/compat.py index 4a51de8..59fab30 100644 --- a/graphene_django/compat.py +++ b/graphene_django/compat.py @@ -5,7 +5,11 @@ class MissingType(object): 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, - JSONField, RangeField) + from django.contrib.postgres.fields import ( + ArrayField, + HStoreField, + JSONField, + RangeField, + ) except ImportError: ArrayField, HStoreField, JSONField, RangeField = (MissingType,) * 4 diff --git a/graphene_django/debug/sql/types.py b/graphene_django/debug/sql/types.py index 850ced4..eeef482 100644 --- a/graphene_django/debug/sql/types.py +++ b/graphene_django/debug/sql/types.py @@ -3,9 +3,7 @@ from graphene import Boolean, Float, ObjectType, String class DjangoDebugSQL(ObjectType): class Meta: - description = ( - "Represents a single database query made to a Django managed DB." - ) + description = "Represents a single database query made to a Django managed DB." vendor = String( required=True, @@ -14,37 +12,26 @@ class DjangoDebugSQL(ObjectType): ), ) alias = String( - required=True, - description="The Django database alias (e.g. 'default').", + 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.", + required=True, description="Duration of this database query in seconds." ) raw_sql = String( - required=True, - description="The raw SQL of this query, without params.", + 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.", + 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.", + required=True, description="Whether this database query was a SELECT." ) # Postgres diff --git a/graphene_django/debug/types.py b/graphene_django/debug/types.py index cda5725..1cd816d 100644 --- a/graphene_django/debug/types.py +++ b/graphene_django/debug/types.py @@ -7,7 +7,4 @@ class DjangoDebug(ObjectType): class Meta: description = "Debugging information for the current query." - sql = List( - DjangoDebugSQL, - description="Executed SQL queries for this API query.", - ) + sql = List(DjangoDebugSQL, description="Executed SQL queries for this API query.") diff --git a/graphene_django/filter/filterset.py b/graphene_django/filter/filterset.py index 4059083..7676ea8 100644 --- a/graphene_django/filter/filterset.py +++ b/graphene_django/filter/filterset.py @@ -45,8 +45,7 @@ class GrapheneFilterSetMixin(BaseFilterSet): FILTER_DEFAULTS = dict( itertools.chain( - FILTER_FOR_DBFIELD_DEFAULTS.items(), - GRAPHENE_FILTER_SET_OVERRIDES.items() + FILTER_FOR_DBFIELD_DEFAULTS.items(), GRAPHENE_FILTER_SET_OVERRIDES.items() ) ) @@ -59,7 +58,6 @@ if VERSION[0] < 2: from django.utils.text import capfirst class GrapheneFilterSetMixinPython2(GrapheneFilterSetMixin): - @classmethod def filter_for_reverse_field(cls, f, name): """Handles retrieving filters for reverse relationships diff --git a/graphene_django/forms/tests/test_mutation.py b/graphene_django/forms/tests/test_mutation.py index df0ffd5..543e89e 100644 --- a/graphene_django/forms/tests/test_mutation.py +++ b/graphene_django/forms/tests/test_mutation.py @@ -13,7 +13,7 @@ class MyForm(forms.Form): class PetForm(forms.ModelForm): class Meta: model = Pet - fields = '__all__' + fields = "__all__" def test_needs_form_class(): @@ -66,7 +66,7 @@ class ModelFormMutationTests(TestCase): class PetMutation(DjangoModelFormMutation): class Meta: form_class = PetForm - exclude_fields = ['id'] + exclude_fields = ["id"] self.assertEqual(PetMutation._meta.model, Pet) self.assertEqual(PetMutation._meta.return_field_name, "pet") @@ -102,7 +102,9 @@ class ModelFormMutationTests(TestCase): pet = Pet.objects.create(name="Axel", age=10) - result = PetMutation.mutate_and_get_payload(None, None, id=pet.pk, name="Mia", age=10) + result = PetMutation.mutate_and_get_payload( + None, None, id=pet.pk, name="Mia", age=10 + ) self.assertEqual(Pet.objects.count(), 1) pet.refresh_from_db() @@ -132,7 +134,6 @@ class ModelFormMutationTests(TestCase): # A pet was not created self.assertEqual(Pet.objects.count(), 0) - fields_w_error = [e.field for e in result.errors] self.assertEqual(len(result.errors), 2) self.assertIn("name", fields_w_error) diff --git a/graphene_django/management/commands/graphql_schema.py b/graphene_django/management/commands/graphql_schema.py index d7f83da..9f8689e 100644 --- a/graphene_django/management/commands/graphql_schema.py +++ b/graphene_django/management/commands/graphql_schema.py @@ -64,7 +64,7 @@ class Command(CommandArguments): indent = options.get("indent") schema_dict = {"data": schema.introspect()} - if out == '-': + if out == "-": self.stdout.write(json.dumps(schema_dict, indent=indent, sort_keys=True)) else: self.save_file(out, schema_dict, indent) diff --git a/graphene_django/tests/base_test.py b/graphene_django/tests/base_test.py index 471dffe..84e1dc5 100644 --- a/graphene_django/tests/base_test.py +++ b/graphene_django/tests/base_test.py @@ -11,7 +11,7 @@ class GraphQLTestCase(TestCase): """ # URL to graphql endpoint - GRAPHQL_URL = '/graphql/' + GRAPHQL_URL = "/graphql/" # Here you need to set your graphql schema for the tests GRAPHQL_SCHEMA = None @@ -20,7 +20,9 @@ class GraphQLTestCase(TestCase): super(GraphQLTestCase, cls).setUpClass() if not cls.GRAPHQL_SCHEMA: - raise AttributeError('Variable GRAPHQL_SCHEMA not defined in GraphQLTestCase.') + raise AttributeError( + "Variable GRAPHQL_SCHEMA not defined in GraphQLTestCase." + ) cls._client = Client(cls.GRAPHQL_SCHEMA) @@ -37,14 +39,15 @@ class GraphQLTestCase(TestCase): Returns: Response object from client """ - body = {'query': query} + body = {"query": query} if op_name: - body['operation_name'] = op_name + body["operation_name"] = op_name if input_data: - body['variables'] = {'input': input_data} + body["variables"] = {"input": input_data} - resp = self._client.post(self.GRAPHQL_URL, json.dumps(body), - content_type='application/json') + resp = self._client.post( + self.GRAPHQL_URL, json.dumps(body), content_type="application/json" + ) return resp def assertResponseNoErrors(self, resp): @@ -55,7 +58,7 @@ class GraphQLTestCase(TestCase): """ content = json.loads(resp.content) self.assertEqual(resp.status_code, 200) - self.assertNotIn('errors', list(content.keys())) + self.assertNotIn("errors", list(content.keys())) def assertResponseHasErrors(self, resp): """ @@ -63,4 +66,4 @@ class GraphQLTestCase(TestCase): :resp HttpResponse: Response """ content = json.loads(resp.content) - self.assertIn('errors', list(content.keys())) + self.assertIn("errors", list(content.keys())) diff --git a/graphene_django/tests/test_command.py b/graphene_django/tests/test_command.py index fa78aec..dbabafa 100644 --- a/graphene_django/tests/test_command.py +++ b/graphene_django/tests/test_command.py @@ -10,14 +10,18 @@ def test_generate_file_on_call_graphql_schema(savefile_mock, settings): assert "Successfully dumped GraphQL schema to schema.json" in out.getvalue() -@patch('json.dump') +@patch("json.dump") def test_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='') + 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" + 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" diff --git a/graphene_django/tests/test_converter.py b/graphene_django/tests/test_converter.py index eac5851..bb176b3 100644 --- a/graphene_django/tests/test_converter.py +++ b/graphene_django/tests/test_converter.py @@ -241,8 +241,7 @@ def test_should_manytoone_convert_connectionorlist(): class Meta: model = Article - graphene_field = convert_django_field(Reporter.articles.rel, - A._meta.registry) + 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) @@ -255,8 +254,7 @@ def test_should_onetoone_reverse_convert_model(): class Meta: model = FilmDetails - graphene_field = convert_django_field(Film.details.related, - A._meta.registry) + 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) diff --git a/graphene_django/views.py b/graphene_django/views.py index 9a530de..0b840f9 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -126,8 +126,7 @@ class GraphQLView(View): if show_graphiql: return self.render_graphiql( - request, - graphiql_version=self.graphiql_version, + request, graphiql_version=self.graphiql_version ) if self.batch: diff --git a/setup.py b/setup.py index 3431cd5..e622a71 100644 --- a/setup.py +++ b/setup.py @@ -24,6 +24,12 @@ tests_require = [ "pytest-django>=3.3.2", ] + rest_framework_require + +dev_requires = [ + "black==19.3b0", + "flake8==3.7.7", +] + tests_require + setup( name="graphene-django", version=version, @@ -58,7 +64,7 @@ setup( setup_requires=["pytest-runner"], tests_require=tests_require, rest_framework_require=rest_framework_require, - extras_require={"test": tests_require, "rest_framework": 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", From 6f03597a5e5d09a894ae93fc9cb97b963fcbe8a9 Mon Sep 17 00:00:00 2001 From: Paul Hallett Date: Mon, 6 May 2019 13:28:02 +0100 Subject: [PATCH 14/26] Create CODE_OF_CONDUCT.md --- CODE_OF_CONDUCT.md | 76 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..5560ba2 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# 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 From 31468f56874532c4628905063a891f8260198990 Mon Sep 17 00:00:00 2001 From: Paul Hallett Date: Fri, 26 Apr 2019 16:48:37 +0100 Subject: [PATCH 15/26] Rebuild documentation --- README.md | 8 +- README.rst | 12 +- docs/authorization.rst | 6 +- docs/conf.py | 89 +++++----- docs/debug.rst | 1 + docs/filtering.rst | 48 ++++- docs/form-mutations.rst | 74 -------- docs/index.rst | 23 ++- docs/installation.rst | 69 ++++++++ docs/introspection.rst | 4 +- docs/mutations.rst | 229 ++++++++++++++++++++++++ docs/queries.rst | 270 +++++++++++++++++++++++++++++ docs/rest-framework.rst | 64 ------- docs/schema.rst | 50 ++++++ docs/tutorial-plain.rst | 24 +-- docs/tutorial-relay.rst | 6 +- examples/cookbook/cookbook/urls.py | 2 +- 17 files changed, 766 insertions(+), 213 deletions(-) delete mode 100644 docs/form-mutations.rst create mode 100644 docs/installation.rst create mode 100644 docs/mutations.rst create mode 100644 docs/queries.rst delete mode 100644 docs/rest-framework.rst create mode 100644 docs/schema.rst diff --git a/README.md b/README.md index d2fe4b6..159a592 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,11 @@ Please read [UPGRADE-v2.0.md](https://github.com/graphql-python/graphene/blob/ma A [Django](https://www.djangoproject.com/) integration for [Graphene](http://graphene-python.org/). -## Installation +## Documentation + +[Visit the documentation to get started!](https://docs.graphene-python.org/projects/django/en/latest/) + +## Quickstart For installing graphene, just run this command in your shell @@ -39,7 +43,7 @@ from graphene_django.views import GraphQLView urlpatterns = [ # ... - url(r'^graphql', GraphQLView.as_view(graphiql=True)), + url(r'^graphql$', GraphQLView.as_view(graphiql=True)), ] ``` diff --git a/README.rst b/README.rst index 2e0593d..44feaee 100644 --- a/README.rst +++ b/README.rst @@ -10,8 +10,14 @@ to learn how to upgrade to Graphene ``2.0``. A `Django `__ integration for `Graphene `__. -Installation ------------- + +Documentation +------------- + +`Visit the documentation to get started! `__ + +Quickstart +---------- For installing graphene, just run this command in your shell @@ -46,7 +52,7 @@ serve the queries. urlpatterns = [ # ... - url(r'^graphql', GraphQLView.as_view(graphiql=True)), + url(r'^graphql$', GraphQLView.as_view(graphiql=True)), ] Examples diff --git a/docs/authorization.rst b/docs/authorization.rst index 3b34326..3d0bb8a 100644 --- a/docs/authorization.rst +++ b/docs/authorization.rst @@ -155,7 +155,7 @@ To restrict users from accessing the GraphQL API page the standard Django LoginR .. code:: python #views.py - + from django.contrib.auth.mixins import LoginRequiredMixin from graphene_django.views import GraphQLView @@ -171,9 +171,9 @@ For Django 1.9 and below: urlpatterns = [ # some other urls - url(r'^graphql', PrivateGraphQLView.as_view(graphiql=True, schema=schema)), + url(r'^graphql$', PrivateGraphQLView.as_view(graphiql=True, schema=schema)), ] - + For Django 2.0 and above: .. code:: python diff --git a/docs/conf.py b/docs/conf.py index 2ea2d55..a485d5b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,6 +1,6 @@ import os -on_rtd = os.environ.get('READTHEDOCS', None) == 'True' +on_rtd = os.environ.get("READTHEDOCS", None) == "True" # -*- coding: utf-8 -*- # @@ -34,46 +34,44 @@ on_rtd = os.environ.get('READTHEDOCS', None) == 'True' # 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', + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinx.ext.viewcode", ] if not on_rtd: - extensions += [ - 'sphinx.ext.githubpages', - ] + extensions += ["sphinx.ext.githubpages"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +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' +source_suffix = ".rst" # The encoding of source files. # # source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'Graphene Django' -copyright = u'Graphene 2017' -author = u'Syrus Akbary' +project = u"Graphene Django" +copyright = u"Graphene 2017" +author = u"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 = u'1.0' +version = u"1.0" # The full version, including alpha/beta/rc tags. -release = u'1.0.dev' +release = u"1.0.dev" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -94,7 +92,7 @@ language = None # 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'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The reST default role (used for this markup: `text`) to use for all # documents. @@ -116,7 +114,7 @@ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] @@ -175,7 +173,7 @@ html_theme_path = [sphinx_graphene_theme.get_html_theme_path()] # 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'] +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 @@ -255,34 +253,30 @@ html_static_path = ['_static'] # html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. -htmlhelp_basename = 'Graphenedoc' +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', + # 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', u'Graphene Documentation', - u'Syrus Akbary', 'manual'), + (master_doc, "Graphene.tex", u"Graphene Documentation", u"Syrus Akbary", "manual") ] # The name of an image file (relative to this directory) to place at the top of @@ -323,8 +317,7 @@ latex_documents = [ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'graphene_django', u'Graphene Django Documentation', - [author], 1) + (master_doc, "graphene_django", u"Graphene Django Documentation", [author], 1) ] # If true, show URL addresses after external links. @@ -338,9 +331,15 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'Graphene-Django', u'Graphene Django Documentation', - author, 'Graphene Django', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "Graphene-Django", + u"Graphene Django Documentation", + author, + "Graphene Django", + "One line description of project.", + "Miscellaneous", + ) ] # Documents to append as an appendix to all manuals. @@ -414,7 +413,7 @@ epub_copyright = copyright # epub_post_files = [] # A list of files that should not be packed into the epub file. -epub_exclude_files = ['search.html'] +epub_exclude_files = ["search.html"] # The depth of the table of contents in toc.ncx. # @@ -446,4 +445,4 @@ epub_exclude_files = ['search.html'] # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://docs.python.org/': None} +intersphinx_mapping = {"https://docs.python.org/": None} diff --git a/docs/debug.rst b/docs/debug.rst index 8ef2e86..8e67c23 100644 --- a/docs/debug.rst +++ b/docs/debug.rst @@ -34,6 +34,7 @@ And in your ``settings.py``: .. code:: python GRAPHENE = { + ... 'MIDDLEWARE': [ 'graphene_django.debug.DjangoDebugMiddleware', ] diff --git a/docs/filtering.rst b/docs/filtering.rst index feafd40..d02366f 100644 --- a/docs/filtering.rst +++ b/docs/filtering.rst @@ -136,7 +136,7 @@ pre-filter animals owned by the authenticated user (set in ``context.user``). class AnimalFilter(django_filters.FilterSet): # Do case-insensitive lookups on 'name' - name = django_filters.CharFilter(lookup_type='iexact') + name = django_filters.CharFilter(lookup_type=['iexact']) class Meta: model = Animal @@ -146,3 +146,49 @@ pre-filter animals owned by the authenticated user (set in ``context.user``). 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=( + ('created_at', 'created_at'), + ) + ) + + class Group(DjangoObjectType): + users = DjangoFilterConnectionField(Ticket, filterset_class=UserFilter) + + class Meta: + name = 'Group' + model = GroupModel + 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 + } + } + } \ No newline at end of file diff --git a/docs/form-mutations.rst b/docs/form-mutations.rst deleted file mode 100644 index 85b89e8..0000000 --- a/docs/form-mutations.rst +++ /dev/null @@ -1,74 +0,0 @@ -Integration with Django forms -============================= - -Graphene-Django comes with mutation classes that will convert the fields on Django forms into inputs on a mutation. -*Note: the API is experimental and will likely change in the future.* - -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 - - 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. diff --git a/docs/index.rst b/docs/index.rst index 9469c29..c7820cf 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,17 +1,34 @@ Graphene-Django =============== -Contents: +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 tenants +------------ + +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: 0 + :maxdepth: 1 + installation tutorial-plain tutorial-relay + schema + queries + mutations filtering authorization debug rest-framework - form-mutations introspection testing diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 0000000..8f3e550 --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,69 @@ +Installation +============ + +Graphene-Django takes a few seconds to install and set up. + +Requirements +------------ + +Graphene-Django currently supports the following versions of Django: + +* Django 2.X + +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: + +.. code:: python + + from django.conf.urls import url + from graphene_django.views import GraphQLView + + urlpatterns = [ + # ... + url(r'^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): + pass + + schema = graphene.Schema(query=Query) + + +To learn how to extend the schema object for your project, read the basic tutorial. \ No newline at end of file diff --git a/docs/introspection.rst b/docs/introspection.rst index bd80f26..92e3612 100644 --- a/docs/introspection.rst +++ b/docs/introspection.rst @@ -5,8 +5,8 @@ Relay uses `Babel Relay Plugin `__ that requires you to provide your GraphQL schema data. -Graphene comes with a management command for Django to dump your schema -data to ``schema.json`` that is compatible with babel-relay-plugin. +Graphene comes with a Django management command to dump your schema +data to ``schema.json`` which is compatible with babel-relay-plugin. Usage ----- diff --git a/docs/mutations.rst b/docs/mutations.rst new file mode 100644 index 0000000..f6c6f14 --- /dev/null +++ b/docs/mutations.rst @@ -0,0 +1,229 @@ +Mutations +========= + +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 + + + 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) + + def mutate(self, 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: + 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 + + 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. + + +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 imoprt 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 imoprt 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 import relay, 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)) + 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. \ No newline at end of file diff --git a/docs/queries.rst b/docs/queries.rst new file mode 100644 index 0000000..d54c908 --- /dev/null +++ b/docs/queries.rst @@ -0,0 +1,270 @@ +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.types import DjangoObjectType + from .models import Question + + + class QuestionType(DjangoObjectType): + class Meta: + model = Question + + + class Query: + questions = graphene.List(QuestionType) + question = graphene.Field(Question, question_id=graphene.String()) + + def resolve_questions(self, info, **kwargs): + # Querying a list + return Question.objects.all() + + def resolve_question(self, info, question_id): + # Querying a single question + return Question.objects.get(pk=question_id) + + +Fields +------ + +By default, ``DjangoObjectType`` will present all fields on a Model through GraphQL. +If you don't want to do this you can change this by setting either ``only_fields`` and ``exclude_fields``. + +only_fields +~~~~~~~~~~~ + +Show **only** these fields on the model: + +.. code:: python + + class QuestionType(DjangoObjectType): + class Meta: + model = Question + only_fields = ('question_text') + + +exclude_fields +~~~~~~~~~~~~~~ + +Show all fields **except** those in ``exclude_fields``: + +.. code:: python + + class QuestionType(DjangoObjectType): + class Meta: + model = Question + exclude_fields = ('question_text') + + +Customised fields +~~~~~~~~~~~~~~~~~ + +You can completely overwrite a field, or add new fields, to a ``DjangoObjectType`` using a Resolver: + +.. code:: python + + class QuestionType(DjangoObjectType): + + class Meta: + model = Question + exclude_fields = ('question_text') + + extra_field = graphene.String() + + def resolve_extra_field(self, info): + return 'hello!' + + +Related models +-------------- + +Say you have the following models: + +.. code:: python + + 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 + + class QuestionType(DjangoObjectType): + class Meta: + model = Question + only_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 + + class CategoryType(DjangoObjectType): + class Meta: + model = Category + +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 + + @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(self, info, **kwargs): + +Where "foo" is the name of the field declared in the ``Query`` object. + +.. code:: python + + class Query: + foo = graphene.List(QuestionType) + + def resolve_foo(self, info, **kwargs): + id = kwargs.get('id') + return QuestionModel.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 + + class Query: + question = graphene.Field(Question, foo=graphene.String(), bar=graphene.Int()) + + def resolve_question(self, info, foo, bar): + # 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 + + def resolve_questions(self, info, **kwargs): + # See if a user is authenticated + if info.context.user.is_authenticated(): + return Question.objects.all() + else: + return Question.objects.none() + + +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: + question = graphene.Field(MyQuestion, question_id=graphene.String()) + + def resolve_question(self, 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 +~~~~~~~~~~~~ + +.. code:: python + + from graphene import relay + from graphene_django import DjangoObjectType + from .models import Question + + + class QuestionType(DjangoObjectType): + class Meta: + model = Question + interaces = (relay.Node,) + + + class QuestionConnection(relay.Connection): + class Meta: + node = QuestionType + + + class Query: + question = graphene.Field(QuestionType) + questions = relay.ConnectionField(QuestionConnection) + +See the `Relay documentation `__ on +the core graphene pages for more information on customing the Relay experience. \ No newline at end of file diff --git a/docs/rest-framework.rst b/docs/rest-framework.rst deleted file mode 100644 index ce666de..0000000 --- a/docs/rest-framework.rst +++ /dev/null @@ -1,64 +0,0 @@ -Integration with Django Rest Framework -====================================== - -You can re-use your Django Rest Framework serializer with -graphene django. - - -Mutation --------- - -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. The update -operation looks up models by the primary key by default. You can -customize the look up with the lookup attribute. - -.. code:: python - - from graphene_django.rest_framework.mutation import SerializerMutation - - 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 - - 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} diff --git a/docs/schema.rst b/docs/schema.rst new file mode 100644 index 0000000..9f0c283 --- /dev/null +++ b/docs/schema.rst @@ -0,0 +1,50 @@ +Schema +====== + +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 `__ \ No newline at end of file diff --git a/docs/tutorial-plain.rst b/docs/tutorial-plain.rst index a87b011..29df56e 100644 --- a/docs/tutorial-plain.rst +++ b/docs/tutorial-plain.rst @@ -1,12 +1,9 @@ -Introduction tutorial - Graphene and Django +Basic Tutorial =========================================== -Graphene has a number of additional features that are designed to make -working with Django *really simple*. - -Our primary focus here is to give a good understanding of how to connect models from Django ORM to graphene object types. - -A good idea is to check the `graphene `__ documentation first. +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 ------------------------- @@ -91,7 +88,7 @@ Don't forget to create & run migrations: python manage.py makemigrations python manage.py migrate - + Load some test data ^^^^^^^^^^^^^^^^^^^ @@ -108,7 +105,7 @@ following: $ 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``). @@ -255,7 +252,7 @@ aforementioned GraphiQL we specify that on the parameters with ``graphiql=True`` urlpatterns = [ url(r'^admin/', admin.site.urls), - url(r'^graphql', GraphQLView.as_view(graphiql=True)), + url(r'^graphql$', GraphQLView.as_view(graphiql=True)), ] @@ -273,7 +270,7 @@ as explained above, we can do so here using: urlpatterns = [ url(r'^admin/', admin.site.urls), - url(r'^graphql', GraphQLView.as_view(graphiql=True, schema=schema)), + url(r'^graphql$', GraphQLView.as_view(graphiql=True, schema=schema)), ] @@ -487,7 +484,7 @@ Now, with the code in place, we can query for single objects. For example, lets query ``category``: -.. code:: +.. code:: query { category(id: 1) { @@ -536,3 +533,6 @@ Summary As you can see, GraphQL is very powerful but there are a lot of repetitions in our example. We can do a lot of improvements by adding layers of abstraction on top of ``graphene-django``. If you want to put things like ``django-filter`` and automatic pagination in action, you should continue with the **relay tutorial.** + +A good idea is to check the `graphene `__ +documentation but it is not essential to understand and use Graphene-Django in your project. \ No newline at end of file diff --git a/docs/tutorial-relay.rst b/docs/tutorial-relay.rst index 630898e..5f8bd64 100644 --- a/docs/tutorial-relay.rst +++ b/docs/tutorial-relay.rst @@ -1,4 +1,4 @@ -Graphene and Django Tutorial using Relay +Relay tutorial ======================================== Graphene has a number of additional features that are designed to make @@ -244,7 +244,7 @@ aforementioned GraphiQL we specify that on the params with ``graphiql=True``. urlpatterns = [ url(r'^admin/', admin.site.urls), - url(r'^graphql', GraphQLView.as_view(graphiql=True)), + url(r'^graphql$', GraphQLView.as_view(graphiql=True)), ] @@ -262,7 +262,7 @@ as explained above, we can do so here using: urlpatterns = [ url(r'^admin/', admin.site.urls), - url(r'^graphql', GraphQLView.as_view(graphiql=True, schema=schema)), + url(r'^graphql$', GraphQLView.as_view(graphiql=True, schema=schema)), ] diff --git a/examples/cookbook/cookbook/urls.py b/examples/cookbook/cookbook/urls.py index 9f8755b..4bf6003 100644 --- a/examples/cookbook/cookbook/urls.py +++ b/examples/cookbook/cookbook/urls.py @@ -6,5 +6,5 @@ from graphene_django.views import GraphQLView urlpatterns = [ url(r'^admin/', admin.site.urls), - url(r'^graphql', GraphQLView.as_view(graphiql=True)), + url(r'^graphql$', GraphQLView.as_view(graphiql=True)), ] From 15b5e6ae246b8626f9338ea89e101794fe457d38 Mon Sep 17 00:00:00 2001 From: Paul Hallett Date: Tue, 7 May 2019 19:26:19 +0100 Subject: [PATCH 16/26] Fix security issues --- examples/cookbook-plain/requirements.txt | 2 +- examples/cookbook/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/cookbook-plain/requirements.txt b/examples/cookbook-plain/requirements.txt index 539fd67..2154fd8 100644 --- a/examples/cookbook-plain/requirements.txt +++ b/examples/cookbook-plain/requirements.txt @@ -1,4 +1,4 @@ graphene graphene-django graphql-core>=2.1rc1 -django==2.1.2 +django==2.1.6 diff --git a/examples/cookbook/requirements.txt b/examples/cookbook/requirements.txt index b2ace1f..3fed30f1 100644 --- a/examples/cookbook/requirements.txt +++ b/examples/cookbook/requirements.txt @@ -1,5 +1,5 @@ graphene graphene-django graphql-core>=2.1rc1 -django==1.9 +django==1.11.19 django-filter>=2 From df4a07982f553cb8dbbe7e66c47fd48cdd0a3486 Mon Sep 17 00:00:00 2001 From: Paul Hallett Date: Tue, 7 May 2019 20:22:08 +0100 Subject: [PATCH 17/26] Add documentation for settings --- docs/index.rst | 2 +- docs/settings.rst | 103 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 docs/settings.rst diff --git a/docs/index.rst b/docs/index.rst index c7820cf..602f8dd 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -29,6 +29,6 @@ For more advanced use, check out the Relay tutorial. filtering authorization debug - rest-framework introspection testing + settings diff --git a/docs/settings.rst b/docs/settings.rst new file mode 100644 index 0000000..bd09886 --- /dev/null +++ b/docs/settings.rst @@ -0,0 +1,103 @@ +Settings +======== + +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 Dictonary 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, + } \ No newline at end of file From bd53940d2322762d86d950361c61e63755972af9 Mon Sep 17 00:00:00 2001 From: Paul Hallett Date: Tue, 7 May 2019 20:23:26 +0100 Subject: [PATCH 18/26] newline --- docs/settings.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/settings.rst b/docs/settings.rst index bd09886..547e77f 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -100,4 +100,4 @@ Default: ``100`` GRAPHENE = { 'RELAY_CONNECTION_MAX_LIMIT': 100, - } \ No newline at end of file + } From 2bf7e7f66daf3f17c0ec8e599a32769eac24d849 Mon Sep 17 00:00:00 2001 From: Paul Hallett Date: Wed, 8 May 2019 22:45:28 +0100 Subject: [PATCH 19/26] Fix importing error for GraphQLTestCase --- docs/testing.rst | 2 +- graphene_django/utils/__init__.py | 19 +++++++++++++++++++ .../{tests/base_test.py => utils/testing.py} | 4 +--- graphene_django/{ => utils}/utils.py | 7 ------- 4 files changed, 21 insertions(+), 11 deletions(-) create mode 100644 graphene_django/utils/__init__.py rename graphene_django/{tests/base_test.py => utils/testing.py} (95%) rename graphene_django/{ => utils}/utils.py (96%) diff --git a/docs/testing.rst b/docs/testing.rst index a4d5518..b111642 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -9,7 +9,7 @@ Usage: import json - from graphene_django.tests.base_test import GraphQLTestCase + from graphene_django.utils.testing import GraphQLTestCase from my_project.config.schema import schema class MyFancyTestCase(GraphQLTestCase): diff --git a/graphene_django/utils/__init__.py b/graphene_django/utils/__init__.py new file mode 100644 index 0000000..f9c388d --- /dev/null +++ b/graphene_django/utils/__init__.py @@ -0,0 +1,19 @@ +from .utils import ( + DJANGO_FILTER_INSTALLED, + get_reverse_fields, + maybe_queryset, + get_model_fields, + is_valid_django_model, + import_single_dispatch, +) +from .testing import GraphQLTestCase + +__all__ = [ + "DJANGO_FILTER_INSTALLED", + "get_reverse_fields", + "maybe_queryset", + "get_model_fields", + "is_valid_django_model", + "import_single_dispatch", + "GraphQLTestCase", +] diff --git a/graphene_django/tests/base_test.py b/graphene_django/utils/testing.py similarity index 95% rename from graphene_django/tests/base_test.py rename to graphene_django/utils/testing.py index 84e1dc5..47f8d04 100644 --- a/graphene_django/tests/base_test.py +++ b/graphene_django/utils/testing.py @@ -1,8 +1,6 @@ import json -from django.http import HttpResponse -from django.test import Client -from django.test import TestCase +from django.test import TestCase, Client class GraphQLTestCase(TestCase): diff --git a/graphene_django/utils.py b/graphene_django/utils/utils.py similarity index 96% rename from graphene_django/utils.py rename to graphene_django/utils/utils.py index 532be06..02c47ee 100644 --- a/graphene_django/utils.py +++ b/graphene_django/utils/utils.py @@ -4,13 +4,6 @@ from django.db import models from django.db.models.manager import Manager -# from graphene.utils import LazyList - - -class LazyList(object): - pass - - try: import django_filters # noqa From ce9d989bcdabc5e7a4bb3fa5559de0f4604b6a74 Mon Sep 17 00:00:00 2001 From: David Beitey Date: Mon, 13 May 2019 07:01:44 +0000 Subject: [PATCH 20/26] Update install docs for Django 2.x This uses the new URL routing syntax introduced in Django 2.0 (https://docs.djangoproject.com/en/2.2/releases/2.0/#simplified-url-routing-syntax). The older `url()` syntax will deprecated at some point in future https://docs.djangoproject.com/en/2.2/ref/urls/#url --- docs/installation.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 8f3e550..a2dc665 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -30,16 +30,16 @@ Add ``graphene_django`` to the ``INSTALLED_APPS`` in the ``settings.py`` file of ] -We need to add a graphql URL to the ``urls.py`` of your Django project: +We need to add a ``graphql`` URL to the ``urls.py`` of your Django project: .. code:: python - from django.conf.urls import url + from django.urls import path from graphene_django.views import GraphQLView urlpatterns = [ # ... - url(r'^graphql$', GraphQLView.as_view(graphiql=True)), + path("graphql", GraphQLView.as_view(graphiql=True)), ] (Change ``graphiql=True`` to ``graphiql=False`` if you do not want to use the GraphiQL API browser.) From ba64bceab09c33370d5ab8a0c8076abeced57a71 Mon Sep 17 00:00:00 2001 From: zorig Date: Wed, 15 May 2019 17:22:29 +0800 Subject: [PATCH 21/26] graphiql version upgrade --- graphene_django/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/views.py b/graphene_django/views.py index 0b840f9..72cca88 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -51,7 +51,7 @@ def instantiate_middleware(middlewares): class GraphQLView(View): - graphiql_version = "0.11.10" + graphiql_version = "0.11.11" graphiql_template = "graphene/graphiql.html" schema = None From 884c4cce0c3342cf0b27c22ec08e6ee6145f3a49 Mon Sep 17 00:00:00 2001 From: changeling Date: Wed, 15 May 2019 16:27:35 -0500 Subject: [PATCH 22/26] Correct Babel Relay Plugin docs link per Issue 358. See graphql-python#358. --- docs/introspection.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/introspection.rst b/docs/introspection.rst index 92e3612..0fc6776 100644 --- a/docs/introspection.rst +++ b/docs/introspection.rst @@ -1,9 +1,7 @@ Introspection Schema ==================== -Relay uses `Babel Relay -Plugin `__ -that requires you to provide your GraphQL schema data. +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. From 2edf7f4ec0926159347c45df3ba1ef327d465706 Mon Sep 17 00:00:00 2001 From: changeling Date: Wed, 15 May 2019 17:02:26 -0500 Subject: [PATCH 23/26] Correct examples/cookbook settings.py. See https://github.com/graphql-python/graphene-django/issues/455. --- examples/cookbook-plain/cookbook/settings.py | 6 +----- examples/cookbook/cookbook/settings.py | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/examples/cookbook-plain/cookbook/settings.py b/examples/cookbook-plain/cookbook/settings.py index d846db4..bce2bab 100644 --- a/examples/cookbook-plain/cookbook/settings.py +++ b/examples/cookbook-plain/cookbook/settings.py @@ -56,6 +56,7 @@ MIDDLEWARE = [ GRAPHENE = { 'SCHEMA': 'cookbook.schema.schema', + 'SCHEMA_INDENT': 2, 'MIDDLEWARE': ( 'graphene_django.debug.DjangoDebugMiddleware', ) @@ -130,8 +131,3 @@ USE_TZ = True # https://docs.djangoproject.com/en/1.9/howto/static-files/ STATIC_URL = '/static/' - -GRAPHENE = { - 'SCHEMA': 'cookbook.schema.schema', - 'SCHEMA_INDENT': 2, -} diff --git a/examples/cookbook/cookbook/settings.py b/examples/cookbook/cookbook/settings.py index 948292d..0b3207e 100644 --- a/examples/cookbook/cookbook/settings.py +++ b/examples/cookbook/cookbook/settings.py @@ -57,6 +57,7 @@ MIDDLEWARE_CLASSES = [ GRAPHENE = { 'SCHEMA': 'cookbook.schema.schema', + 'SCHEMA_INDENT': 2, 'MIDDLEWARE': ( 'graphene_django.debug.DjangoDebugMiddleware', ) @@ -131,8 +132,3 @@ USE_TZ = True # https://docs.djangoproject.com/en/1.9/howto/static-files/ STATIC_URL = '/static/' - -GRAPHENE = { - 'SCHEMA': 'cookbook.schema.schema', - 'SCHEMA_INDENT': 2, -} From 04fe299a6e71b15d638dbbb3a48e6c8f8dc50d3d Mon Sep 17 00:00:00 2001 From: changeling Date: Wed, 15 May 2019 19:50:55 -0500 Subject: [PATCH 24/26] Corrected docs/queries.rst. (#633) * Corrected typos in docs/queries.rst. * Add basic resolvers to Relay Full example in docs/queries.rst. Added basic resolvers to Full example in Relay section. * Remove question and question resolver. * Add query example to queries.rst. Added query example in Relay section. Minor clean-up. --- docs/queries.rst | 72 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 67 insertions(+), 5 deletions(-) diff --git a/docs/queries.rst b/docs/queries.rst index d54c908..0edd1dd 100644 --- a/docs/queries.rst +++ b/docs/queries.rst @@ -30,7 +30,7 @@ Full example class Query: questions = graphene.List(QuestionType) - question = graphene.Field(Question, question_id=graphene.String()) + question = graphene.Field(QuestionType, question_id=graphene.String()) def resolve_questions(self, info, **kwargs): # Querying a list @@ -243,6 +243,8 @@ 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 @@ -254,7 +256,7 @@ Full example class QuestionType(DjangoObjectType): class Meta: model = Question - interaces = (relay.Node,) + interfaces = (relay.Node,) class QuestionConnection(relay.Connection): @@ -263,8 +265,68 @@ Full example class Query: - question = graphene.Field(QuestionType) questions = relay.ConnectionField(QuestionConnection) -See the `Relay documentation `__ on -the core graphene pages for more information on customing the Relay experience. \ No newline at end of file + def resolve_questions(root, info, **kwargs): + return Question.objects.all() + + +You can now execute queries like: + + +.. code:: python + + { + questions (first: 2, after: "YXJyYXljb25uZWN0aW9uOjEwNQ==") { + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + id + question_text + } + } + } + } + +Which returns: + +.. code:: python + + { + "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. From 49aedf171abc176bd20839dd309109c7400b9fc0 Mon Sep 17 00:00:00 2001 From: Dulmandakh Date: Mon, 20 May 2019 17:48:28 +0800 Subject: [PATCH 25/26] bump graphiql to 0.13.0, and rename __debug to _debug due to __ limitations --- docs/debug.rst | 8 +++---- examples/cookbook-plain/cookbook/schema.py | 2 +- examples/cookbook/cookbook/schema.py | 2 +- graphene_django/debug/tests/test_query.py | 28 +++++++++++----------- graphene_django/views.py | 2 +- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/debug.rst b/docs/debug.rst index 8e67c23..d1cbb21 100644 --- a/docs/debug.rst +++ b/docs/debug.rst @@ -15,7 +15,7 @@ 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')``. +* Add the ``debug`` field into the schema root ``Query`` with the value ``graphene.Field(DjangoDebug, name='_debug')``. .. code:: python @@ -24,7 +24,7 @@ For use the Django Debug plugin in Graphene: class Query(graphene.ObjectType): # ... - debug = graphene.Field(DjangoDebug, name='__debug') + debug = graphene.Field(DjangoDebug, name='_debug') schema = graphene.Schema(query=Query) @@ -59,11 +59,11 @@ the GraphQL request, like: } } # Here is the debug field that will output the SQL queries - __debug { + _debug { sql { rawSql } } } -Note that the ``__debug`` field must be the last field in your query. +Note that the ``_debug`` field must be the last field in your query. diff --git a/examples/cookbook-plain/cookbook/schema.py b/examples/cookbook-plain/cookbook/schema.py index f8606a7..f91d62c 100644 --- a/examples/cookbook-plain/cookbook/schema.py +++ b/examples/cookbook-plain/cookbook/schema.py @@ -8,7 +8,7 @@ from graphene_django.debug import DjangoDebug class Query(cookbook.ingredients.schema.Query, cookbook.recipes.schema.Query, graphene.ObjectType): - debug = graphene.Field(DjangoDebug, name='__debug') + debug = graphene.Field(DjangoDebug, name='_debug') schema = graphene.Schema(query=Query) diff --git a/examples/cookbook/cookbook/schema.py b/examples/cookbook/cookbook/schema.py index f8606a7..f91d62c 100644 --- a/examples/cookbook/cookbook/schema.py +++ b/examples/cookbook/cookbook/schema.py @@ -8,7 +8,7 @@ from graphene_django.debug import DjangoDebug class Query(cookbook.ingredients.schema.Query, cookbook.recipes.schema.Query, graphene.ObjectType): - debug = graphene.Field(DjangoDebug, name='__debug') + debug = graphene.Field(DjangoDebug, name='_debug') schema = graphene.Schema(query=Query) diff --git a/graphene_django/debug/tests/test_query.py b/graphene_django/debug/tests/test_query.py index f2ef096..592899b 100644 --- a/graphene_django/debug/tests/test_query.py +++ b/graphene_django/debug/tests/test_query.py @@ -31,7 +31,7 @@ def test_should_query_field(): class Query(graphene.ObjectType): reporter = graphene.Field(ReporterType) - debug = graphene.Field(DjangoDebug, name="__debug") + debug = graphene.Field(DjangoDebug, name="_debug") def resolve_reporter(self, info, **args): return Reporter.objects.first() @@ -41,7 +41,7 @@ def test_should_query_field(): reporter { lastName } - __debug { + _debug { sql { rawSql } @@ -50,7 +50,7 @@ def test_should_query_field(): """ expected = { "reporter": {"lastName": "ABA"}, - "__debug": { + "_debug": { "sql": [{"rawSql": str(Reporter.objects.order_by("pk")[:1].query)}] }, } @@ -75,7 +75,7 @@ def test_should_query_list(): class Query(graphene.ObjectType): all_reporters = graphene.List(ReporterType) - debug = graphene.Field(DjangoDebug, name="__debug") + debug = graphene.Field(DjangoDebug, name="_debug") def resolve_all_reporters(self, info, **args): return Reporter.objects.all() @@ -85,7 +85,7 @@ def test_should_query_list(): allReporters { lastName } - __debug { + _debug { sql { rawSql } @@ -94,7 +94,7 @@ def test_should_query_list(): """ expected = { "allReporters": [{"lastName": "ABA"}, {"lastName": "Griffin"}], - "__debug": {"sql": [{"rawSql": str(Reporter.objects.all().query)}]}, + "_debug": {"sql": [{"rawSql": str(Reporter.objects.all().query)}]}, } schema = graphene.Schema(query=Query) result = schema.execute( @@ -117,7 +117,7 @@ def test_should_query_connection(): class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) - debug = graphene.Field(DjangoDebug, name="__debug") + debug = graphene.Field(DjangoDebug, name="_debug") def resolve_all_reporters(self, info, **args): return Reporter.objects.all() @@ -131,7 +131,7 @@ def test_should_query_connection(): } } } - __debug { + _debug { sql { rawSql } @@ -145,9 +145,9 @@ def test_should_query_connection(): ) assert not result.errors assert result.data["allReporters"] == expected["allReporters"] - assert "COUNT" in result.data["__debug"]["sql"][0]["rawSql"] + assert "COUNT" in result.data["_debug"]["sql"][0]["rawSql"] query = str(Reporter.objects.all()[:1].query) - assert result.data["__debug"]["sql"][1]["rawSql"] == query + assert result.data["_debug"]["sql"][1]["rawSql"] == query def test_should_query_connectionfilter(): @@ -166,7 +166,7 @@ def test_should_query_connectionfilter(): class Query(graphene.ObjectType): all_reporters = DjangoFilterConnectionField(ReporterType, fields=["last_name"]) s = graphene.String(resolver=lambda *_: "S") - debug = graphene.Field(DjangoDebug, name="__debug") + debug = graphene.Field(DjangoDebug, name="_debug") def resolve_all_reporters(self, info, **args): return Reporter.objects.all() @@ -180,7 +180,7 @@ def test_should_query_connectionfilter(): } } } - __debug { + _debug { sql { rawSql } @@ -194,6 +194,6 @@ def test_should_query_connectionfilter(): ) assert not result.errors assert result.data["allReporters"] == expected["allReporters"] - assert "COUNT" in result.data["__debug"]["sql"][0]["rawSql"] + assert "COUNT" in result.data["_debug"]["sql"][0]["rawSql"] query = str(Reporter.objects.all()[:1].query) - assert result.data["__debug"]["sql"][1]["rawSql"] == query + assert result.data["_debug"]["sql"][1]["rawSql"] == query diff --git a/graphene_django/views.py b/graphene_django/views.py index 72cca88..c9ac770 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -51,7 +51,7 @@ def instantiate_middleware(middlewares): class GraphQLView(View): - graphiql_version = "0.11.11" + graphiql_version = "0.13.0" graphiql_template = "graphene/graphiql.html" schema = None From 7690c2c0025f1537ee9c9c2971d3c9b1546b9ba6 Mon Sep 17 00:00:00 2001 From: Dulmandakh Date: Mon, 20 May 2019 19:41:25 +0800 Subject: [PATCH 26/26] bump react to 16.8.6 --- graphene_django/templates/graphene/graphiql.html | 6 ++---- graphene_django/views.py | 5 ++++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/graphene_django/templates/graphene/graphiql.html b/graphene_django/templates/graphene/graphiql.html index c0c9af1..d0fb5a8 100644 --- a/graphene_django/templates/graphene/graphiql.html +++ b/graphene_django/templates/graphene/graphiql.html @@ -23,11 +23,9 @@ add "&raw" to the end of the URL within a browser. - - diff --git a/graphene_django/views.py b/graphene_django/views.py index 0b840f9..2fc8c88 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -53,6 +53,7 @@ def instantiate_middleware(middlewares): class GraphQLView(View): graphiql_version = "0.11.10" graphiql_template = "graphene/graphiql.html" + react_version = "16.8.6" schema = None graphiql = False @@ -126,7 +127,9 @@ class GraphQLView(View): if show_graphiql: return self.render_graphiql( - request, graphiql_version=self.graphiql_version + request, + graphiql_version=self.graphiql_version, + react_version=self.react_version, ) if self.batch: