Compare commits

...

378 Commits
v2.3.2 ... main

Author SHA1 Message Date
Firas Kafri
c52cf2b045
Bump version to 3.2.3 2025-03-13 11:29:45 +03:00
Florian Zimmermann
e69e4a0399
Bugfix: call resolver function in DjangoConnectionField as documented (#1529)
* treat warnings as errors when running the tests

* silence warnings

* bugfix: let DjangoConnectionField call its resolver function

that is, the one specified using DjangoConnectionField(..., resolver=some_func)

* ignore the DeprecationWarning about typing.ByteString in graphql
2025-03-13 11:25:48 +03:00
Sergey Fursov
97deb761e9
fix typed choices, make working with different Django 5x choices options (#1539)
* fix typed choices, make working with different Django 5x choices options

* remove `graphene_django/compat.py` from ruff exclusions
2025-03-13 11:23:51 +03:00
Sergey Fursov
8d4a64a40d
add official Django 5.1 support (#1540) 2024-12-27 13:46:47 +08:00
Alexandre Detiste
269225085d
remove dead code: singledispatch has been in the standard library ... (#1534)
* remove dead code: singledispatch has been in the stard library for many years

(BTW this function does not seems to be used anywhere anymore)

* lint
2024-09-15 21:50:15 +07:00
Markus Richter
28c71c58f7 Bump to 3.2.2 2024-06-12 10:52:45 +08:00
Kien Dang
6f21dc7a94
Not require explicitly set ordering in DjangoConnectionField (#1518)
* Revert "feat!: check django model has a default ordering when used in a relay connection (#1495)"

This reverts commit 96c09ac439.

* Fix assert no warning for pytest>=8
2024-04-18 12:00:31 +08:00
Ülgen Sarıkavak
ea45de02ad
Make use of http.HTTPStatus for response status code checks (#1487) 2024-04-09 03:43:34 +03:00
dependabot[bot]
eac113e136
Bump django from 3.2.24 to 3.2.25 in /examples/cookbook (#1508)
Bumps [django](https://github.com/django/django) from 3.2.24 to 3.2.25.
- [Commits](https://github.com/django/django/compare/3.2.24...3.2.25)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-09 03:39:21 +03:00
Kien Dang
d69c90550f
Bump to 3.2.1 (#1512) 2024-04-09 03:37:32 +03:00
Pablo Alexis Domínguez Grau
3f813d4679
Fix ReadTheDocs builds (#1509)
* Add RTD config file

* Doc fixes to reference main branch instead of master
2024-03-29 12:11:56 +08:00
Alisson Patricio
45c2aa09b5
Allows field's choices to be a callable (#1497)
* Allows field's choices to be a callable

Starting in Django 5 field's choices can also be a callable

* test if field with callable choices converts into enum

---------

Co-authored-by: Kien Dang <mail@kien.ai>
2024-03-21 00:48:51 +08:00
Diogo Silva
ac09cd2967
fix: Fix broke 'get_choices' with restframework 3.15.0 (#1506) 2024-03-18 09:58:47 +08:00
dependabot[bot]
54372b41d5
Bump django from 3.1.14 to 3.2.24 in /examples/cookbook (#1498)
Bumps [django](https://github.com/django/django) from 3.1.14 to 3.2.24.
- [Commits](https://github.com/django/django/compare/3.1.14...3.2.24)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-08 10:50:13 +08:00
Thomas Leonard
96c09ac439
feat!: check django model has a default ordering when used in a relay connection (#1495)
Co-authored-by: Thomas Leonard <thomas@loftorbital.com>
2024-01-30 12:09:18 +03:00
Laurent
b85177cebf
fix: same type list (#1492)
* fix: same type list

* chore: improve test
2024-01-20 16:36:00 +08:00
Firas Kafri
4d0484f312
Bump version 2023-12-20 13:22:33 +03:00
Noxx
c416a2b0f5
Provide setting to enable/disable converting choices to enums globally (#1477)
Co-authored-by: Firas Kafri <3097061+firaskafri@users.noreply.github.com>
Co-authored-by: Kien Dang <mail@kien.ai>
2023-12-20 17:55:15 +08:00
Kien Dang
feb7252b8a
Add support for validation rules (#1475)
* Add support for validation rules

* Enable customizing validate max_errors through settings

* Add tests for validation rules

* Add examples for validation rules

* Allow setting validation_rules in class def

* Add tests for validation_rules inherited from parent class

* Make tests for validation rules stricter
2023-12-20 12:48:45 +03:00
Firas Kafri
3a64994e52
Bump version (#1486) 2023-12-20 12:44:40 +03:00
Kien Dang
db2d40ec94
Remove Django 4.1 (EOL) and add Django 5.0 to CI (#1483) 2023-12-14 11:20:54 +03:00
Kien Dang
62126dd467
Add Python 3.12 to CI (#1481) 2023-12-05 22:11:00 +03:00
danthewildcat
e735f5dbdb
Optimize views (#1439)
* Optimize execute_graphql_request

* Require operation_ast to be found by view handler

* Remove unused show_graphiql kwarg

* Old style if syntax

* Revert "Remove unused show_graphiql kwarg"

This reverts commit 33b3426092.

* Add missing schema validation step

* Pass args directly to improve clarity

* Remove duplicated operation_ast not None check

---------

Co-authored-by: Firas Kafri <3097061+firaskafri@users.noreply.github.com>
Co-authored-by: Kien Dang <mail@kien.ai>
2023-10-29 23:42:27 +08:00
Kien Dang
36cf100e8b
Use ruff format to replace black (#1473)
* Use ruff format to replace black

* Adjust ruff config to be compatible with ruff-format

https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules

* Format

* Replace black with ruff format in Makefile
2023-10-25 11:33:00 +03:00
Kien Dang
e8f36b018d
Fix test Client headers for Django 4.2 (#1465)
* Fix test Client headers for Django 4.2

* Lazy import pkg_resources

since it could be quite heavy

* Remove use of pkg_resources altogether
2023-09-18 23:23:53 +08:00
mnasiri
83d3d27f14
Fix graphiql explorer styles by sending graphiql_plugin_explorer_css_sri param to render_graphiql function of the GraphQlView (#1418) (#1460) 2023-09-14 00:26:18 +08:00
Romain Létendart
ee7560f629
Support displaying deprecated input fields in GraphiQL docs (#1458)
* Update GraphiQL docs URL in docs/settings

And deduplicate link declaration.

* Support displaying deprecated input fields in GraphiQL docs
2023-09-13 09:49:01 +03:00
lilac-supernova-2
67def2e074
Typo fixes (#1459)
* Fix Star Wars spaceship name

* Fix some typos in comments

* Typo fixes

* More typo fixes
2023-09-06 10:29:58 +03:00
mahmoudmostafa0
e49a01c189
adding optional_field in Serializermutation to enfore some fields to be optional (#1455)
* adding optional_fields to enforce fields to be optional

* adding support for all

* adding unit tests

* Update graphene_django/rest_framework/mutation.py

Co-authored-by: Kien Dang <kiend@pm.me>

* linting

* linting

* add missing import

---------

Co-authored-by: Kien Dang <kiend@pm.me>
2023-08-28 00:15:35 +03:00
Thomas Leonard
0473f1a9a3
fix: empty list is not an empty value for list filters even when a custom filtering method is provided (#1450)
Co-authored-by: Thomas Leonard <thomas@loftorbital.com>
2023-08-11 23:24:58 +08:00
Kien Dang
720db1f987
Only release on pypi after tests pass (#1452) 2023-08-11 09:51:59 +03:00
Firas Kafri
4ac3f3f42d
Update __init__.py 2023-08-10 01:12:15 +03:00
Firas Kafri
ee7598e71a
Remove typo 2023-08-09 23:41:57 +03:00
Firas Kafri
05d7fb5396
Bump version 2023-08-09 20:49:51 +03:00
Kien Dang
79b4a23ae0
Miscellaneous CI fixes (#1447)
* Update Makefile

* django master requires at least python 3.10 now

* Allow customizing options passed to tox -e pre-commit

* py.test -> pytest

* Update ruff

* Fix E721

Do not compare types, use `isinstance()`

* Add back black to dev dependencies

* Pin black and ruff versions
2023-08-09 20:48:42 +03:00
Laurent
db34d2e815
fix: foreign key nullable and custom resolver (#1446)
* fix: nullable one to one relation

* fix: makefile
2023-08-09 20:28:26 +03:00
Kien Dang
9a773b9d7b
Use ruff in pre-commit (#1441)
* Use ruff in pre-commit

* Add pyupgrade

* Add isort

* Add bugbear

* Fix B015 Pointless comparison

* Fix B026

* B018 false positive

* Remove flake8 and isort config from setup.cfg

* Remove black and flake8 from dev dependencies

* Update black

* Show list of fixes applied with autofix on

* Fix typo

* Add C4 flake8-comprehensions

* Add ruff to dev dependencies

* Fix up
2023-08-06 01:47:00 +03:00
Kien Dang
45a732f1db
Prevent duplicate CI runs, also work with PRs from forks (#1443)
* Prevent duplicate CI runs

* Trigger CI on pull requests from forks
2023-08-06 01:45:10 +03:00
Kien Dang
5eb5fe294a
Remove Python 3.7 (EOL since EOL since 2023-06-27) from CI (#1440)
* Remove Python 3.7 (EOL since EOL since 2023-06-27) from CI

* Remove unused context

* Use pyupgrade --py38-plus in pre-commit
2023-08-04 11:15:23 +03:00
James
5d7a04fce9
Update mutation.py to serialize Enum objects into input values (#1431)
* Fix for issue #1385: Update mutation.py to serialize Enum objects into input values for ChoiceFields

* Update graphene_django/rest_framework/mutation.py

Co-authored-by: Steven DeMartini <1647130+sjdemartini@users.noreply.github.com>

---------

Co-authored-by: Steven DeMartini <1647130+sjdemartini@users.noreply.github.com>
2023-07-27 02:41:40 +03:00
Firas Kafri
3172710d12
exclude 'fans' from ReporterForm tests (#1434) 2023-07-18 20:35:51 +03:00
Tom Dror
b1abebdb97
Support base class relations and reverse for proxy models (#1380)
* support reverse relationship for proxy models

* support multi table inheritence

* update query test for multi table inheritance

* remove debugger

* support local many to many in model inheritance

* format and lint

---------

Co-authored-by: Firas K <3097061+firaskafri@users.noreply.github.com>
2023-07-18 20:17:45 +03:00
Laurent
0de35ca3b0
fix: fk resolver permissions leak (#1411)
* fix: fk resolver permissions leak

* fix: only one query for 1o1 relation

* tests: added queries count check

* fix: docstring

* fix: typo

* docs: added warning to authorization

* feat: added bypass_get_queryset decorator
2023-07-18 15:16:52 +03:00
Firas Kafri
2fafa881a8
Bump version 2023-07-18 15:13:58 +03:00
Steven DeMartini
cd43022283
Maintain JSONField in graphene_django.compat module (#1429)
Fixes https://github.com/graphql-python/graphene-django/issues/1428

This should improve backwards compatibility, fixing issues in downstream
packages (notably graphene-django-cud
https://github.com/tOgg1/graphene-django-cud/issues/109, and also
graphene-django-extras, both of which depended on
`graphene_django.compat.JSONField`).

Co-authored-by: Steven DeMartini <sjdemartini@users.noreply.github.com>
2023-07-18 15:11:30 +03:00
Jeongseok Kang
3f061a0c50
docs: Update location of GraphQL Relay Specification (#1432) 2023-07-18 15:10:22 +03:00
Firas Kafri
e950164c8e
Bump version to 3.1.2 2023-06-17 09:29:18 +03:00
Steven DeMartini
2358bd30a4
Update compat.py MissingType results after PGJSONField removal (#1423)
As mentioned in https://github.com/graphql-python/graphene-django/pull/1421/files#r1221711648
2023-06-07 20:06:37 +03:00
Dulmandakh
3e7a16af73
CI: remove Django 4.0 (#1422)
* CI: remove Django 4.0

* fix tags
2023-06-07 17:36:51 +03:00
Dulmandakh
8fa8aea3c0
remove JSONField compat (#1421)
* remove JSONFIeld compat

* fix black
2023-06-07 17:36:29 +03:00
Dulmandakh
c925a32dc3
CI: add Django 4.2 (#1420)
* CI: add Django 4.2

* fix tox
2023-06-07 16:52:40 +03:00
Sezgin ACER
8934393909
Add check for serializers.HiddenField on fields_for_serializer function (#1419)
* Add check for `serializers.HiddenField` on fields_for_serializer function

* Add pre-commit changes
2023-06-06 09:20:32 +03:00
Steven DeMartini
520ddeabf6
Fix graphiql explorer styles by including official CSS, and update local example app for testing (#1418)
* Add venv and .venv to gitignore since common venv paths

* Update cookbook-plain app requirements and local-dev notes

This also adds the DEFAULT_AUTO_FIELD to the app's Django settings to
resolve this warning when running `migrate`:

```
ingredients.Category: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'django.db.models.AutoField'.
	HINT: Configure the DEFAULT_AUTO_FIELD setting or the IngredientsConfig.default_auto_field attribute to point to a subclass of AutoField, e.g. 'django.db.models.BigAutoField'.
```

* Fix #1417 graphiql explorer styles by including official CSS

Like in the official graphiql-plugin-explorer example here
6198646919/packages/graphiql-plugin-explorer/examples/index.html (L26-L29)

Resolves https://github.com/graphql-python/graphene-django/issues/1417

* Update GraphiQL version

---------

Co-authored-by: Steven DeMartini <sjdemartini@users.noreply.github.com>
Co-authored-by: Kien Dang <mail@kien.ai>
2023-06-02 11:48:53 +03:00
Kien Dang
38709d8396
Correct schema write test (#1416)
<Mock>.called_once() just returns a Mock, so assert <Mock>.called_once()
always passes. We want <Mock>.assert_called_once().
2023-05-27 16:53:22 +03:00
Kien Dang
63fd98393f
Set pypi GH action to latest v1 (#1415) 2023-05-27 16:26:52 +03:00
Firas Kafri
4e5acd4702
Fix linting issues (#1412) 2023-05-24 16:13:23 +03:00
Firas Kafri
ebf49431e9
Bump version 2023-05-24 16:10:22 +03:00
Firas Kafri
b75904d4c8
long_description_content_type='text/markdown' 2023-05-24 16:07:45 +03:00
Firas Kafri
7fe661d423
Bump version 2023-05-24 16:03:14 +03:00
ndpu
be17278b49
Add DjangoFormInputObjectType to forms/types (#1325)
* Add DjangoFormInputObjectType to forms/types

InputObjectType derived class which gets fields from django form.
Type of fields with choices (converted to enum) is set to custom scalar
type (using Meta.object_type) to dynamically convert enum values back.

* Correct Reporter model a_choice field type according to CHOICES tuple

* Add tests for DjangoFormInputObjectType

* Add pyenv files to .gitignore

* Fix pyupgrade

* Fix tests

* Add docs

* Fix docs example

---------

Co-authored-by: Firas Kafri <3097061+firaskafri@users.noreply.github.com>
2023-05-24 15:58:50 +03:00
ndpu
a6596273cf
Update docs/requirements.txt (#1410)
* change Sphinx version from 1.5.3 to 7.0.0

* change sphinx-autobuild version from 0.7.1 to 2021.3.14

* add pygments-graphql-lexer to docs/requirements.txt
2023-05-24 15:55:20 +03:00
Mykhailo Havelia
388ca41d64
fix: use execution_context_class attribute for GraphQLView (#1398)
* fix: use execution_context_class attribute for GraphQLView
2023-05-24 15:54:44 +03:00
Firas Kafri
72a3700856
Update Development Status calassifier (#1409) 2023-05-05 13:04:47 +03:00
shukryzablah
09f9b6d2f1
Remove redundant call to validate (#1393)
* Remove redundant call to validate

The call to `validate` in the django view is redundant with the validation call in graphql-core.

* Remove whitespace

---------

Co-authored-by: Firas K <3097061+firaskafri@users.noreply.github.com>
2023-05-05 13:04:22 +03:00
Firas Kafri
6f13d28b6e
Update README.md (#1408)
* Update README.md

* Delete README.rst

* Update long_description source to be from README.md
2023-05-04 23:54:09 +03:00
Firas Kafri
ce7492b5ae
Delete README.rst 2023-05-04 23:46:15 +03:00
Kien Dang
52f992183f
Add GraphiQL Explorer plugin (#1397) 2023-05-04 22:06:10 +03:00
Ülgen Sarıkavak
8540a9332c
Add support for Python 3.11 (#1365)
* Add support for Python 3.11

* Fix Python 3.11 compatibility matrix

* Add temporary fix for default enum description

---------

Co-authored-by: Firas Kafri <firaskafri@Firass-MacBook-Pro-2.local>
2023-05-04 15:19:24 +03:00
Firas K
af8888f58e
Upgrade github actions versions, default python and dev dependencies (#1407)
* Use Python 3.10 for deployments on PyPi

* Update gh-action-pypi-publish version

* Update python version

* Update checkout and setup-python versions

* Upgrade dev dependencies

* fromat examples and few files to follow black new version

* Upgrade pytest version

---------

Co-authored-by: Firas Kafri <firaskafri@Firass-MacBook-Pro-2.local>
2023-05-03 13:25:16 +03:00
Steven DeMartini
c1a22bfd91 Add pre-commit to dev-setup
pre-commit is currently configured nicely but hasn't been part of the
Makefile setup and isn't mentioned in the contributing notes. This
change makes it so that pre-commit is installed as a part of the dev
setup, whereas before it had to be manually installed.
2023-05-03 12:08:22 +03:00
Kien Dang
95a0642818 fix: fix graphiql request failure 2023-05-03 12:07:54 +03:00
Firas K
a8ceca77ed Bump version 2023-05-03 11:54:46 +03:00
Steven DeMartini
20a6cecc4c Add test validating query performance with select_related + prefetch_related
This test passes after reverting the `CustomField` resolver change
introduced in
https://github.com/graphql-python/graphene-django/pull/1315, but fails
with that resolver code present. For instance, adding back the resolver
code gives a test failure showing:

```
Failed: Expected to perform 2 queries but 11 were done
```

This should ensure there aren't regressions that prevent
query-optimization in the future.
2023-05-03 11:37:17 +03:00
Steven DeMartini
9796e93fc7 Remove obsolete tests and add note about rationale 2023-05-03 11:37:17 +03:00
Steven DeMartini
f67c5dbc8c Revert field resolver logic to fix poor query performance
This reverts the change to `convert_field_to_djangomodel` introduced in
https://github.com/graphql-python/graphene-django/pull/1315 for the
reasons discussed here
https://github.com/graphql-python/graphene-django/pull/1315/files#r1015659857.
As mentioned there, without reverting this code, "queries are forced
every time an object is resolved, making an exponential number of
queries when nesting without any possibility of optimizing".

That regression prevented `graphene-django-optimizer` from working with
`graphene-django` v3.0.0b9+ (where this change first was published), as
discussed in
https://github.com/graphql-python/graphene-django/issues/1356#issuecomment-1284718187,
https://github.com/tfoxy/graphene-django-optimizer/issues/86, and
https://github.com/tfoxy/graphene-django-optimizer/pull/83#issuecomment-1451987397.

For now, this marks the two tests that depended on this problematic code
as "expected to fail", and perhaps they can be reintroduced if there's a
way to support this logic in a way that does not prevent
`select_related` and `prefetch_related` query-optimization and introduce
nested N+1s.

As mentioned here
https://github.com/graphql-python/graphene-django/pull/1315#issuecomment-1468594361,
this is blocking upgrade to graphene-django v3 for many users, and
fixing this would allow many to begin upgrading and contributing to keep
graphene-django going.
2023-05-03 11:37:17 +03:00
Firas K
34cc86063b
☂️ v3.0.1 ☂️ 2023-04-29 20:26:39 +03:00
Firas K
a335042dbe
☂️ v3.0.1 ☂️ 2023-04-29 20:26:05 +03:00
Firas K
df3c0bf75b Update filtering.rst 2023-04-26 22:46:40 +03:00
Firas K
7e1a1d1fb8 Update django-filter url 2023-04-26 22:46:40 +03:00
Kien Dang
3283d0b1be Update GraphiQL to 2.4.1 2023-04-17 09:17:21 +03:00
Bendik Eger
1d814c54c4 Fix schema print with -.graphql 2023-04-10 14:52:10 +03:00
Josh Warwick
0beb3385df import error resolved? 2023-03-01 11:13:00 +03:00
Josh Warwick
3b41aaf7bf Remove promise based middleware 2023-03-01 11:13:00 +03:00
Kien Dang
d18cab8aa4 Update graphiql to 1.4.7 2022-12-25 01:09:13 +03:00
Ülgen Sarıkavak
daa0ab046b
Update pre-commit tools (#1364) 2022-11-23 01:16:14 +03:00
Omar Mirza
a000d58514 Clarify cookbook example READMEs
Currently the relay cookbook's readme has a link to the plain tutorial
page. The plain cookbook readme also instructs the user to change
directory into the directory for the relay example. This change fixes
both issues.

Also changed the title for the relay example to specify that it uses
relay.
2022-11-15 09:56:28 +03:00
Yuekui
86c5309c45
Fix broken UT due to pytest import error (#1368) 2022-11-14 13:56:18 +03:00
Nikolai Røed Kristiansen
4517e32224
👷 Add pre-commit (#1336)
* 🔧 Add pre-commit config

Similar to graphene and graphene-sqlalchemy

* ⬆ Bump black

* 👷 Lint on CI

* ⬆ Bump flake8-black

* 🔧 Keep excluding migrations

* ⬆ Bump flake8

* 🔧 Remove black and flake8 from tox config

* ⬆ Update pre-commit versions

* Upgrade syntax to python 3.7+

* Format with pre-commit

dedent docs/schema.py to allow formatting

* Fix tests on python 3.7
2022-10-19 17:10:30 +03:00
Syrus Akbary
f24cbd5148
Fix custom foreignkey resolvers (#1361)
* Fix custom foreignkey resolvers

* Fixed assert name conversion

* Fix lint
2022-10-17 16:57:24 +02:00
Firas K
ed7c995d8c
☂️ v3.0.0 ☂️ (#1355) 2022-09-26 15:08:32 +03:00
Syberen van Munster
bb03306075
Move testing endpoint to settings (#1105)
* Import testing endpoint from graphene settings

* Add documentation for TESTING_ENDPOINT setting

* Remove empty lines

* Run formatter

Co-authored-by: Firas K <3097061+firaskafri@users.noreply.github.com>
2022-09-26 01:56:22 +03:00
Firas K
c697e5c8c1
Bump version to v3.0.0b9 (#1353) 2022-09-26 01:27:22 +03:00
Suyeol Jeon
a78114ada3
Add support to persist GraphQL headers in GraphiQL (#1209) 2022-09-24 17:41:14 +03:00
Alan Rivas
07940aa5f5
Update tutorial-relay.rst (#1220) 2022-09-24 16:03:45 +03:00
Gabriel Lacroix
9a60589732
Make instructions runnable without tweaking (#1224)
Introduces two changes to make sure the instructions in the tutorial don't require debugging:

- Add `cd ..` when first syncing the database so that `manage.py` is accessible in the working directory.
- Change `cookbook.ingredients.apps.IngredientsConfig.name` to `cookbook.ingredients` from `ingredients` to prevent the following exception:

  ```python
django.core.exceptions.ImproperlyConfigured: Cannot import 'ingredients'. Check that 'cookbook.ingredients.apps.IngredientsConfig.name' is correct.
```
2022-09-24 16:02:33 +03:00
Forest Anderson
0b2cc4ecb2
Fixed deprecation warning (#1313) 2022-09-24 16:00:45 +03:00
belkka
97442f9cee
Fix code examples in queries.rst (#1265)
* Fix code examples in queries.rst

Code example in Arguments section doesn't work as stated in its comment — if "foo" or "bar" are not declare in the graphql query, it will be an error, not they become None.

Code example in Info section has invalid indentation, `resolve_questions()` seems to be a `Query` method, but it's indented as module-level function.

* Fix indentation in query examples

* Enable syntax highlight for graphql queries
2022-09-24 16:00:22 +03:00
belkka
60b3032014
Fix type hint for DjangoObjectTypeOptions.model (#1269)
Proper type is `typing.Type[Model]`, not `Model`
2022-09-24 16:00:12 +03:00
Craig
05d3df92e7
Delay assignment of csrftoken (#1289) 2022-09-24 15:59:53 +03:00
Firas K
541caa117e
Fixes related to pr#1412 (#1352)
* fix: setup.py graphene dependency

* fix: graphene_django/tests/test_get_queryset.py format

Co-authored-by: Firas Kafri <firaskafri@Firass-MacBook-Pro-2.local>
2022-09-24 15:50:40 +03:00
Semyon Pupkov
0f40da7b31
Make errors in form mutation non nullable (#1286) 2022-09-23 11:47:10 +03:00
Thomas Leonard
5d81ba04f9
fix: unit test for graphene pr#1412 (#1315)
* Issue #1111: foreign key should also call get_queryset method

* fix: test for graphene PR https://github.com/graphql-python/graphene/pull/1412

Co-authored-by: Thomas Leonard <thomas@loftorbital.com>
2022-09-23 11:45:02 +03:00
Firas K
b2f83eb277
Bump version to 3.0.0b8 (#1348) 2022-09-23 11:38:11 +03:00
andrei-datcu
56892d7f4b
Cast translated description for DecimalField (#1255)
* Cast translated description for DecimalField

https://github.com/graphql-python/graphene-django/pull/976 casts all the description fields to strings to prevent schema printing from failing whenever the description is a lazy translated string. The `DecimalField` however got in after the v3 merge and it currently misses the cast.

* Fix row size
2022-09-22 19:13:30 +01:00
Yiğit Y. Er
4f315c365d
minor fix on schema.py part (#1306)
The documentation already suggests importing ObjectType from graphene, graphene.ObjectType is not necessary while defining the Query class.
2022-09-22 19:10:52 +01:00
Thomas Leonard
a53ded611b
feat: update name of DjangoFilterConnectionField type input to be consistent with graphene (Issue #1316) (#1317)
Co-authored-by: Thomas Leonard <thomas@loftorbital.com>
2022-09-22 19:09:29 +01:00
Thomas Leonard
37848fa2df
fix: convert Django BigIntegerField to BigInt GraphQL type (#1318)
Co-authored-by: Thomas Leonard <thomas@loftorbital.com>
2022-09-22 19:09:11 +01:00
Thomas Leonard
3473fe025e
fix: backward pagination (#1346)
Co-authored-by: Thomas Leonard <thomas@loftorbital.com>
Co-authored-by: Laurent  <laurent.riviere.pro@gmail.com>
2022-09-22 16:01:28 +01:00
Thomas Leonard
42a40b4df0
chore: update dev dependencies (#1345)
Co-authored-by: Thomas Leonard <thomas@loftorbital.com>
2022-09-22 10:26:21 +01:00
Nikolai Røed Kristiansen
8ae576394e
💥 Stop supporting EOL djangos and pythons (#1337)
* 💥 Stop supporting EOL djangos and pythons

* 👷 Run only supported version in test workflow
2022-09-19 14:31:04 +02:00
Thomas Leonard
2aeb86ba3b
fix: backward pagination indexing error when using bigger last argument than total number of elements (#1344)
Co-authored-by: Thomas Leonard <thomas@loftorbital.com>
2022-09-06 14:00:13 +02:00
Nikolai Røed Kristiansen
5f1731dca3
Fix: Use .formatted instead of format_error (#1327) & Fix tests
* 👽 Use .formatted instead of format_error

*  Fix test with newer graphene

null default values (graphql-python/graphene@03277a5)
no more trailing newlines
2022-08-15 11:41:39 +02:00
Aaron Forsander
f6ec0689c1
Fix documentation references: op_name -> operation_name (#1312) 2022-03-03 16:58:48 +03:00
Peter Paul Kiefer
0bb9f1ca60
I found another wrong link in the filter dokumentation see #1309 (#1311)
* fixed broken links to graphene filter documentation (master->main)

* #1295 There is still a wrong link to github

The referenced example is in main branch
but the link goes to the master branch which still exists.

Co-authored-by: Peter Paul Kiefer <dafisppk@gmail.com>
2022-02-13 08:50:53 +03:00
Peter Paul Kiefer
bf8fd7696b
fixed broken links to graphene filter documentation (master->main) (#1309)
Co-authored-by: Peter Paul Kiefer <dafisppk@gmail.com>
2022-02-12 17:31:45 +03:00
Keith
775644b536
Update requirements to the official graphene 3.0 release (#1290) 2022-01-22 20:04:30 +00:00
Jarkko Piiroinen
e1a7d19833
Convert DecimalField to Decimal instead of Float in DRF and form converters (#1277)
* Convert serializer DecimalField to Decimal

* Convert form DecimalField to Decimal
2022-01-18 17:03:08 +03:00
Tim Schilling
5d5d7f1815
Django v4, python 3.10 support for graphene-django v3 (#1281)
Co-authored-by: Yair Silbermintz <MisterGlass@users.noreply.github.com>
2022-01-07 20:26:07 +00:00
dependabot[bot]
32667b5407
Bump django from 3.1.8 to 3.1.14 in /examples/cookbook (#1283)
Bumps [django](https://github.com/django/django) from 3.1.8 to 3.1.14.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.1.8...3.1.14)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-10 12:58:03 +03:00
Chouaib Lammas
ef9d67302e
Fix ingredient model (#1258)
Add the required positional argument: 'on_delete'
2021-12-10 12:51:10 +03:00
dependabot[bot]
1e4b03b975
Bump django from 3.1.8 to 3.1.14 in /examples/cookbook-plain (#1282)
Bumps [django](https://github.com/django/django) from 3.1.8 to 3.1.14.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.1.8...3.1.14)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-10 12:49:16 +03:00
Paul Bailey
e7f7d8da07
Add missing auto fields (#1212)
* add missing auto fields

* add missing auto fields

* skip small auto field sometimes

* make small auto optional

* make small auto optional
2021-06-11 13:41:02 -07:00
Rainshaw
623d0f219e
update js version (#1188) 2021-04-20 23:05:46 -07:00
Eero Ruohola
608af578d4
Fix broken form.save() call in DjangoFormMutation.perform_mutate (#1155)
Django's plain (non-model) forms don't have the `save` method, so
calling this would just result in an `AttributeError` before this
change.

Resolves #1152
2021-04-10 20:30:15 -07:00
dependabot[bot]
26a851a523
Bump django from 3.1.6 to 3.1.8 in /examples/cookbook-plain (#1157)
Bumps [django](https://github.com/django/django) from 3.1.6 to 3.1.8.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.1.6...3.1.8)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-10 19:28:41 -07:00
dependabot[bot]
762eaabd04
Bump django from 3.1.6 to 3.1.8 in /examples/cookbook (#1156)
Bumps [django](https://github.com/django/django) from 3.1.6 to 3.1.8.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.1.6...3.1.8)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-10 19:28:12 -07:00
Thomas Leonard
80ea51fc3b
Add typed filters (v3) (#1148)
* feat: add TypedFilter which allow to explicitly give a filter input GraphQL type

* Fix doc typo

Co-authored-by: Thomas Leonard <thomas@loftorbital.com>
2021-03-31 10:31:45 -07:00
Kuba Misiorny
3cf940d0c8
Add ability to pass execution_context_class to GraphQLView.as_view() (#1109)
* Add ability to pass `execution_context_class` to `GraphQLView.as_view()`

Currently when passing `execution_context_class` like this:

```
GraphQLView.as_view(execution_context_class=CustomContext)
```

you get the following error from `View.as_view()`
```
TypeError: GraphQLView() received an invalid keyword 'execution_context_class'. as_view only accepts arguments that are already attributes of the class.
```

this PR fixes the `hasattr` check in `.as_view`.

Fixes: #1072

* make black happy

removed whitespace
2021-03-31 10:31:20 -07:00
Ülgen Sarıkavak
3058118e8f
Tox & actions updates (#1143)
* Update Django's main branch name
* Add Python 3.9 to tox
* Update base gh action versions
* Add Django 3.2 to tests
* Remove redundant Django 1.11 references
* Update setup.py for new Django and Python versions
2021-03-24 09:32:37 +03:00
dependabot[bot]
594ca6e25e
Bump django from 3.0.7 to 3.1.6 in /examples/cookbook (#1150)
Bumps [django](https://github.com/django/django) from 3.0.7 to 3.1.6.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.0.7...3.1.6)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-20 22:38:50 -07:00
dependabot[bot]
573d38e13e
Bump django from 3.0.7 to 3.1.6 in /examples/cookbook-plain (#1149)
Bumps [django](https://github.com/django/django) from 3.0.7 to 3.1.6.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.0.7...3.1.6)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-20 22:38:38 -07:00
Tonye Jack
212524fd8c
Cleaned up unused imports and variables. (#1146)
* Cleaned up used imports and variables.
* Optimized imports.
* Fixed mixed imports.
2021-03-18 12:26:02 +03:00
Sebastián Sastoque H
fe66b48d38
Fix main branch tests failing due to wrong instancing of Missing class (#1135)
Co-authored-by: Sebastian Hernandez <sebastian@rhinoafrica.com>
2021-03-02 10:46:35 -08:00
Jason Kraus
e9f25ecf2d
enhancement: DjangoDebugContext captures exceptions and allows captured stack traces to be queried (#1122) 2021-03-02 10:45:46 -08:00
Thomas Leonard
6046a710c8
fix: declaration of required variable in filters v3 (#1137)
* fix: declaration of required variable

* Add unit test

* Fix formating

Co-authored-by: Thomas Leonard <thomas@loftorbital.com>
2021-02-26 14:44:43 -08:00
Sebastián Sastoque H
ea593b673f
Fix: Use resolver passed as an attribute (#1131)
Co-authored-by: Sebastian Hernandez <sebastian@rhinoafrica.com>
2021-02-23 09:50:19 -08:00
Tonye Jack
5cee41407c
Added GraphQLTransactionTestCase (#1099)
* Added GraphQLTransactionTestCase

- Adds support for testing code that is executed within a transaction

Reference: https://docs.djangoproject.com/en/3.1/topics/testing/tools/#django.test.TransactionTestCase
```
 For instance, you cannot test that a block of code is executing within a transaction, as is required when using select_for_update(). In those cases, you should use TransactionTestCase.
```

* Update testing.py

* Update testing.py

* Fixed formatting.

* Updated docs.

* Updated test.

* Update testing.rst
2021-02-22 20:24:02 -08:00
Thomas Leonard
2d4ca0ac7b
Add enum support to filters and fix filter typing (v3) (#1119)
* - Add filtering support for choice fields converted to graphql Enum (or not)
- Fix type of various filters (used to default to String)
- Fix bug with contains introduced in previous PR
- Fix bug with declared filters being overridden (see PR #1108)
- Fix support for ArrayField and add documentation

* Fix for v3

Co-authored-by: Thomas Leonard <thomas@loftorbital.com>
2021-02-22 20:21:32 -08:00
andrei-datcu
5ce4553244
Fix schema dump on windows (#1123)
Without explicitly setting the encoding to "utf-8" I get the following error on windows (python 3.9)

```
  File "D:\env\lib\site-packages\graphene_django\management\commands\graphql_schema.py", line 115, in handle
    self.get_schema(schema, out, indent)
  File "D:\env\lib\site-packages\graphene_django\management\commands\graphql_schema.py", line 72, in get_schema
    self.save_graphql_file(out, schema)                                                                                   
  File "D:\env\lib\site-packages\graphene_django\management\commands\graphql_schema.py", line 59, in save_graphql_file      
    outfile.write(print_schema(schema.graphql_schema))                                                                    
  File "C:\Users\u\AppData\Local\Programs\Python\Python39\lib\encodings\cp1252.py", line 19, in encode 
    return codecs.charmap_encode(input,self.errors,encoding_table)[0]
```
2021-02-22 20:20:59 -08:00
Yves-Gwenael Bourhis
007768b454
Fix subscriptions in JS (#1124) 2021-02-22 20:19:20 -08:00
Jiahao Li
beb2e4aae3
Doc clarification for headers arg in testing utils (#1117)
I think it might be helpful to add an explicit hint that HTTP headers should be prepended with `HTTP_` as required by `django.test.Client` (at least it was not super obvious to me when I tried to use it).
2021-02-22 20:13:49 -08:00
Tonye Jack
52880166bd
Remove unused imports (#1127)
* Remove unused imports

* Update converter.py
2021-02-22 20:10:30 -08:00
Jason Kraus
4573d3db53
Fix test main (#1126)
Co-authored-by: Thomas Leonard <thomas@loftorbital.com>
2021-02-20 14:26:06 -08:00
Jason Kraus
c3404a9793
document purpose of DjangoConnectionField (#1107) 2021-02-02 09:58:43 -08:00
Jason Kraus
d9ab8acf26
document auth pattern: return None with resolve method (#1106)
* document auth pattern: return None with resolve method

* (doc, auth): also show that one can raise an exception in a resolve method
2021-02-02 09:58:21 -08:00
Lucas
5dea6ffa41
Support "contains" and "overlap" filtering (v3) (#1101)
* Support contains/overlap filters

* Remove unused fixtures
2021-01-18 21:39:22 -08:00
Jason Kraus
bcc7f85dad
Add BlankField and mount enums using it v3 (#1096)
* Add BlankField and mount enums using it

* fix lint error from duplicate import

Co-authored-by: Jonathan Kim <jkimbo@gmail.com>
2021-01-11 16:34:50 -08:00
Thomas Leonard
10e48c27b7
Validate in and range filter inputs (#1090)
Co-authored-by: Thomas Leonard <thomas@loftorbital.com>
2021-01-09 19:15:21 -08:00
Ülgen Sarıkavak
ea84827ab8
Fix backward compability on GraphQLTestCase._client setter (#1094) 2021-01-09 19:14:54 -08:00
Lucas
fdeadf5ce5
Fix project setup (#1087)
* Fix project setup

* Fix test_should_query_postgres_fields
2021-01-02 09:46:00 -08:00
Jason Kraus
8324d47999 Merge branch 'v2' into main 2020-12-30 22:31:41 -08:00
Ülgen Sarıkavak
40e5252936
Use the Django TestCase's Client (#1084)
* Use the Django Client test utility instance that Django provides with its TestCase class. This allows GraphQL tests to make use of the stateful client methods like login()

* Add missing test case initializer call

* Don't break backward compability

* Add test for pending deprecation warning on GraphQLTestCase._client

Co-authored-by: Tom Nightingale <tom@tnightingale.com>
2020-12-30 21:12:24 -08:00
Thiago Bellini Ribeiro
8c48516093
Also convert BaseCSVFilter for custom fields (#1081) 2020-12-30 21:03:57 -08:00
Jason Kraus
c049ab7470
WIP: Merge master into v3 (#1086)
* merge master into v3

* fix order_by snake casing by checking if value is None, switch executor to execution_context_class since schema.execute no longer supports executor

* fix linting by removing duplicate defintion and test of convert_form_field_to_string_list
2020-12-30 15:37:57 -08:00
Jason Kraus
2d0b9ddd42
improvement: convert decimal field to graphene decimal (#1083) 2020-12-30 08:25:41 -08:00
Tim Gates
e559a42374
docs: fix simple typo, outputing -> outputting (#1077)
There is a small typo in docs/debug.rst.

Should read `outputting` rather than `outputing`.
2020-12-29 11:30:30 -08:00
Rustam Ganeyev
dab6080fcf
Fixed typo in documentation (#1078)
Added missing kwargs to documentation
2020-12-29 11:30:10 -08:00
Jason Kraus
558288afce v2.14.0 2020-12-22 20:23:41 -08:00
Ignacio Orlandini
8f63199a63
Handle database transactions (#1039)
* Handle Django database atomic requests

* Create and handle database atomic mutations

* Make code compatible with Python 2.7

* Code style

* Define set_rollback instead of using the one in rest_framework.views because of backward compatibility

* Implement mock.patch.dict
2020-12-22 20:18:14 -08:00
Semyon Pupkov
a51c2bffd9
Allow to use camel case in order by field (#1054)
Fixes #1008
2020-12-22 20:15:38 -08:00
Leonardo Arroyo
0e12343853
Fix issue #1055 (#1056)
* Fix issue #1055

* Fix if to elif

* Use self.stdout.write instead of print when printing graphql schema

Co-authored-by: leonardo arroyo <[contato@leonardoarroyo.com](mailto:contato@leonardoarroyo.com)>
2020-12-22 20:13:34 -08:00
Semyon Pupkov
cc3bd05472
Replace Unidecode package with text_unidecode package #1014 (#1060)
Closes #1014
2020-12-22 20:12:22 -08:00
Thomas Leonard
99512c53a1
fix: in and range filters on DjangoFilterConnectionField (#1070)
Co-authored-by: Thomas Leonard <thomas@loftorbital.com>
2020-12-22 20:10:39 -08:00
Jason Kraus
7b35695067
Fix 1061: DjangoListField should not cache queries (#1063)
* fix( DjangoListField ): test that default functionality should resolve/call queryset at view time, first attempt at solution

* fix( DjangoListField ): DjangoListField defines get_manager just like DjangoConnectionField for a better variable name default_manager instead of default_queryset

* fix: apply specific black formatting
2020-12-22 20:10:28 -08:00
Thomas Leonard
454b74052e
Fix backward Relay pagination (#1046)
* Fix backward Relay pagination

* linting

Co-authored-by: Thomas Leonard <thomas@loftorbital.com>
2020-12-22 20:04:45 -08:00
Ülgen Sarıkavak
4c0c821b74
Register MultipleChoiceField as list type (#1033)
In general I welcome reviews even from non-maintainers to build confidence. I haven't seen any objections and this has sat with approval for a week so I am going to go ahead and merge.
2020-11-28 10:30:18 -08:00
Tonye Jack
4b7119d691
Add a default msg to show the response content. (#1064)
* Add a default msg to show the response content.

This seems like an issue with using assertResponseNoErrors and assertResponseHasErrors 

Which doesn't include any errors specific to the response and currently just shows.

```python
    self.assertNotIn("errors", list(content.keys()))
AssertionError: 'errors' unexpectedly found in ['errors', 'data']
```

* Update testing.py
2020-11-27 15:52:42 -08:00
Semyon Pupkov
eb7a0265d8
Use explicit classmethod in simple mutation example (#1059)
rel #1038
2020-11-09 09:06:53 -08:00
Semyon Pupkov
0888c748fd
Change build badge from travis to github actions (#1058) 2020-11-07 21:44:37 -08:00
Nishchit
f554911397
Section added GraphQL testing clients (#919) 2020-11-06 16:04:45 -08:00
Semyon Pupkov
8571bc465a
Improve ordering doc example (#1053) 2020-11-01 10:16:15 -08:00
Thomas Leonard
2140be5e6a
Add offset pagination (#1013)
* Add offset filtering

* Formatting

Co-authored-by: Thomas Leonard <thomas@loftorbital.com>
2020-10-26 09:09:21 -07:00
Roberto Barreda
8408c51bf9
fix variables key in body (#1050) 2020-10-26 09:09:49 +00:00
Jonathan Kim
8928ec2dbe
Restrict DjangoRestFramework version (#1047) 2020-10-19 20:20:30 +01:00
CBuiVNG
65f41c1a17
BUGFIX: don't filter out lookup_field as input (required for update) (#1029) 2020-10-19 17:25:35 +01:00
Andreas Hasenkopf
ee3d4f521f
Include tests and examples in source package, but don't install them (#1034)
..., but don't install them.
Also applied changes as suggested by `black`.
2020-10-19 17:23:41 +01:00
Jonathan Kim
2e806384f6
Update stale.yml 2020-08-27 12:48:29 +01:00
Jonathan Kim
86a66db1f6
Disable stalebot 2020-08-26 16:20:41 +01:00
Josh Warwick
19e3eddddb
Allow passing of meta object in SerializerMutation options (#1028) 2020-08-26 16:03:08 +01:00
DJ Kim
f5d94fda1f
Update testing.rst (#1026)
Co-authored-by: DJ Kim <djkim@paloaltonetworks.com>
2020-08-26 16:01:44 +01:00
Semyon Pupkov
88eefb0e07
Fix testing doc (#1024) 2020-08-26 15:59:43 +01:00
Ülgen Sarıkavak
26960359a2
Add msg params to testing class (#1032) 2020-08-26 15:58:48 +01:00
Varun Dey
6ce208db95
Fix missing colon in function definition (#1030) 2020-08-26 15:57:53 +01:00
Varun Dey
ac1f9ac360
Fix grammar (#1027) 2020-08-24 17:19:53 +01:00
Jonathan Kim
48ed516b5e Trigger tests 2020-08-12 07:29:42 +01:00
Jonathan Kim
b1b57d815a
v3.0.0b6 2020-08-12 07:18:02 +01:00
Jonathan Kim
53023423b6 Fix import 2020-08-12 07:14:48 +01:00
Jonathan Kim
5b1451132d
v2.13.0 2020-08-12 07:10:01 +01:00
Jonathan Kim
33c6a54414 Merge branch 'master' into v3 2020-08-12 07:06:35 +01:00
Jonathan Kim
bd553be10e
Fix JSONField import (#1021) 2020-08-12 07:03:23 +01:00
Nikolai Røed Kristiansen
67a0492c12
Add converter for django 3.1 JSONField (#1017) 2020-08-07 10:22:15 +01:00
Thomas Leonard
11dbde3bea
Fix Connection/Edge naming and add unit test (#1012)
Co-authored-by: Thomas Leonard <thomas@loftorbital.com>
2020-08-07 10:15:35 +01:00
Radosław Kowalski
55769e814f
Add headers support to GraphiQL (#1016)
Co-authored-by: Jonathan Kim <jkimbo@gmail.com>
2020-08-07 10:13:26 +01:00
Jonathan Kim
da9f41c295 Rename get_resolver -> wrap_resolve and fix tests 2020-08-06 20:45:30 +01:00
Jonathan Kim
62f95f2858
v3.0.0b5 2020-08-06 20:35:44 +01:00
Jonathan Kim
6cfcddac5a Merge branch 'master' into v3 2020-08-06 20:35:08 +01:00
Nikolai Røed Kristiansen
2308965658
Extract query function from GraphQLTestCase making it possible to use in a pytest fixture (#1015) 2020-08-05 20:24:16 +01:00
Jonathan Kim
97de26bf2e
Update tutorial docs (#994) 2020-08-05 20:17:53 +01:00
Mel van Londen
b552dcac24 bump version number 2020-07-13 14:12:42 -07:00
Jonathan Kim
63cfbbf59a
Remove operation name from the regex and default to query (#1004) 2020-07-13 14:09:52 -07:00
Mel van Londen
e439bf3727 bump version to 2.12.0 2020-07-12 13:17:03 -07:00
Eric Abruzzese
057b491176
GraphiQL cleanup (#1002)
* Add integrity checks for GraphiQL CDN resources

Also fixes an erroneous assignment preventing a setting from getting to
the UI.

* Pass SRIs and new versions to the template

* Update hashes

* Use SRI-stable artifacts for GraphiQL resources
2020-07-12 12:48:12 -07:00
Eric Abruzzese
6aa6aaaa8c
Update GraphiQL, add GraphiQL subscription support (#1001) 2020-07-12 14:42:31 +01:00
Jonathan Kim
1205e29bef
v2.11.1 2020-07-09 18:02:01 +01:00
Thiago Bellini Ribeiro
d50955a173
Do not break when after is greater than list_length (#999) 2020-07-09 18:01:22 +01:00
Jonathan Kim
965ebdee13 Merge branch 'master' into v3 2020-06-27 11:14:09 +01:00
Jonathan Kim
88f6ec458c v3.0.0b4 2020-06-27 10:44:06 +01:00
Jean-Louis Fuchs
08d0cce55a
Move to_const function from Graphene into Graphene-Django (#992) (#996)
Co-authored-by: Jonathan Kim <jkimbo@gmail.com>
2020-06-27 10:43:25 +01:00
Jonathan Kim
8ddad41bb7 v2.11.0 2020-06-25 17:30:05 +01:00
Jonathan Kim
1bec8e44b7
Move to_const function from Graphene into Graphene-Django (#992) 2020-06-25 15:11:18 +01:00
Jonathan Kim
3026181b28
Set first amount to max limit if not set (#993) 2020-06-25 15:10:56 +01:00
Paul Craciunoiu
3c229b619e
Fix hasNextPage - revert to count. Fix after (#986)
Co-authored-by: Jonathan Kim <jkimbo@gmail.com>
2020-06-25 13:00:24 +01:00
Hubert Siuzdak
3c6733e121
Fix filtering with GlobalIDFilter (#977) 2020-06-25 12:56:06 +01:00
Jonathan Kim
f8b88fdc9a
v3.0.0b3 2020-06-11 11:10:45 +01:00
Radosław Kowalski
1f752b6cad
Warn if fields or exclude are not defined on DjangoObjectType (#981) 2020-06-11 11:09:52 +01:00
Yuyang Zhang(helloqiu)
48bfc395ee
fix(converter): wrap field with NonNull if it is required (#545)
Co-authored-by: Jonathan Kim <jkimbo@gmail.com>
2020-06-10 17:52:45 +01:00
Jonathan Kim
56f1db80cf
Update setup.py classifiers (#987)
Fixes https://github.com/graphql-python/graphene-django/issues/985
2020-06-10 17:41:11 +01:00
DoctorJohn
17146f9b01
Make v3 django choice field enum naming default (in v3) (#982)
Co-authored-by: Jonathan Kim <jkimbo@gmail.com>
2020-06-10 17:32:07 +01:00
DoctorJohn
85976ffb1f
Start raising DeprecationWarnings for using only_fields and exclude_fields (v3) (#980) 2020-06-10 17:30:24 +01:00
DoctorJohn
26c4c48abc
Fix that generated schemas could contain empty descriptions (v3) (#984) 2020-06-10 17:21:37 +01:00
Paul Craciunoiu
c00203499b
DjangoConnectionField slice: use max_limit first, if set (#965) 2020-06-06 19:00:21 +01:00
Jonathan Kim
d9c187ffc2
v3.0.0b2 2020-06-06 17:09:15 +01:00
DoctorJohn
b320b94a73
(v3) Cast potentially translated strings used as descriptions (#976) 2020-06-06 17:08:10 +01:00
dependabot[bot]
40e9c66db3
Bump django from 3.0.3 to 3.0.7 in /examples/cookbook (#979)
Bumps [django](https://github.com/django/django) from 3.0.3 to 3.0.7.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.0.3...3.0.7)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-06-06 12:48:19 +01:00
dependabot[bot]
5ff40d2d14
Bump django from 3.0.3 to 3.0.7 in /examples/cookbook-plain (#978)
Bumps [django](https://github.com/django/django) from 3.0.3 to 3.0.7.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.0.3...3.0.7)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-06-06 12:48:01 +01:00
Jonathan Kim
fb90cb78b3 Merge branch 'master' into v3 2020-05-22 11:17:36 +01:00
Chris Hart
b1f6d41209
fixes minor typo in docs index (#969) 2020-05-22 11:12:27 +01:00
Jonathan Kim
680bf72871
Default camelcase errors (#968) 2020-05-21 16:42:15 +01:00
Padraic Harley
d07642afe6
Error in signature of callproc() and execute() (#966) 2020-05-21 16:16:14 +01:00
Ülgen Sarıkavak
ee120c48e1
Use psycopg2-binary in tox (#964) 2020-05-21 13:18:43 +01:00
Jonathan Kim
d804fe48f2
v2.10.1 2020-05-18 20:20:01 +01:00
vineethvanga18
a987035ef3
fix typo (#959) 2020-05-11 21:03:17 +01:00
Paul Craciunoiu
2225ed62e1
Do not access the internals of SimpleLazyObject (#945) 2020-05-09 17:35:09 +01:00
Jonathan Kim
ed4937f9df
Update bug_report.md 2020-05-09 13:32:20 +01:00
Jonathan Kim
b0901104d8 Update issue templates 2020-05-09 13:30:35 +01:00
Jonathan Kim
0c90006520 Update issue templates 2020-05-09 13:29:37 +01:00
Jonathan Kim
079da60c8f
Create config.yml 2020-05-09 13:28:56 +01:00
Jonathan Kim
72375b2d14 Update issue templates 2020-05-09 13:28:01 +01:00
Jonathan Kim
c79b12b5a3 v3.0.0b1 2020-05-09 12:55:35 +01:00
Jonathan Kim
9b41472922 Merge branch 'master' into v3 2020-05-09 12:50:39 +01:00
Jonathan Kim
709611b577 Install wheel before creating distribution 2020-05-09 12:37:47 +01:00
Jonathan Kim
b0613dd0e4
Update __init__.py 2020-05-09 12:32:51 +01:00
Jonathan Kim
5867331c7b
Allow defining fields as an empty list (#871) 2020-05-09 12:28:19 +01:00
Jonathan Kim
b4e34a5794
Improve DjangoListField (#929) 2020-05-09 12:28:03 +01:00
Jonathan Kim
07c51092e1
Rename op_name to operation_name (#941) 2020-05-09 12:25:24 +01:00
Jonathan Kim
8990e173ac
Add extra types documentation (#902) 2020-05-09 12:19:02 +01:00
Marc Simon
975f45ed1a
GraphQlView: Do not 'instantiate_middleware' if middleware is already a MiddlewareManager (#952) 2020-05-09 12:15:16 +01:00
Jean-Louis Fuchs
10d22de98e
graphql 3.0 and graphene 3.0 final rebase (#951) 2020-05-09 12:13:47 +01:00
Jack W
894c564ab7
Convert nullable BooleanField to nullable Boolean. (#777) 2020-05-09 12:09:17 +01:00
Jonathan Kim
77b9832606 Add GitHub actions (#947) 2020-05-01 14:08:34 +01:00
Jonathan Kim
b8d8508d1f
Add GitHub actions (#947) 2020-05-01 14:04:36 +01:00
kimbriancanavan
82d8dbc893
Convert MultipleChoiceField to List of type String (#611) 2020-04-25 14:22:09 +01:00
Jean-Louis Fuchs
f33223daa7
Make tests order independent (v3) (#940)
* Completes 'Make tests order independent'
2020-04-20 14:01:43 +01:00
Jonathan Kim
9bb0554c94 Merge branch 'master' into v3 2020-04-20 13:24:19 +01:00
Jean-Louis Fuchs
b9f0e4f9ae
Make tests order independent (#932)
* Reset the global registry after each test (teardown)

* Create a settings fixtures that returns graphene_settings and resets
  the graphene_settings after use (teardown)

* Convert test_mutation tests from unittests.TestCase to pytest

* Convert test_mutation PetType to a pet_type fixtures that reregisters
  the type
2020-04-20 13:23:20 +01:00
Noelle Leigh
dc5c971498
Switch operation_name to operationName in GraphQLTestCase (#936)
* Add op_name test

* Replace "operation_name" with "operationName"

* Improve test comments

* Add method for Python 2.7
2020-04-19 21:11:33 +01:00
Sam Millar
23b6419b42
Disable system checks for graphql_schema management command (#939) 2020-04-19 20:42:00 +01:00
Jonathan Kim
657208054a Merge branch 'master' into v3 2020-04-16 14:29:38 +01:00
Jonathan Kim
fba6de41dd
Update README.md 2020-04-13 11:54:17 +01:00
Jonathan Kim
cfc8fea7f5
Update README.md 2020-04-13 11:53:06 +01:00
Jonathan Kim
e1cfc0a80b
v2.9.1 2020-04-12 20:01:56 +01:00
Jonathan Kim
481d3ff35d
Fix DjangoModelFormMutation (#915)
* Fix DjangoModelFormMutation

* Try and fix tests

* Remove unused form
2020-04-12 20:01:30 +01:00
Jonathan Kim
9d9a14c36d
Fix failing tests (#931)
* Use proper model

* Remove failing test

* Add python 3.8 to test list
2020-04-12 16:18:41 +01:00
Roy Segall
3483428f70
Adding documentation for installing django-filter (#928) 2020-04-12 12:57:11 +01:00
Jonathan Kim
613e1e31f4
Add slack link 2020-04-07 11:05:17 +01:00
Ülgen Sarıkavak
dd0d6ef28f
Python 3 (#904)
* Remove Python 2 support

* Upgrade Python & Django versions

* Remove unsupported Django versions
* Remove unsupported Python versions
* Add Python 3.8

* Drop support for django-filter < 2

* Update LoginRequiredMixin doc link

* Remove redundant import

* Resolve RemovedInDjango40Warning warnings

* gql/graphene-django/graphene_django/tests/test_converter.py:175:
RemovedInDjango40Warning: django.utils.translation.ugettext_lazy() is
deprecated in favor of django.utils.translation.gettext_lazy().

* graphene-django/graphene_django/utils/utils.py:28:
RemovedInDjango40Warning: force_text() is deprecated in favor of
force_str().

* No need to use unicode strings with Python3

* Remove singledispatch dependency

singledispatch is inluded with Python >= 3.4, no need for external
package.
2020-04-06 13:21:07 +01:00
fneitzel
b84f61afab
Documentation missing endpoint explanation (#918)
* Documentation missing endpoint explanation

Add some information about GRAPHQL_URL. Otherwise people run into ERROR 400 problems, if they have a different endpoint.

* Update docs/testing.rst

Co-Authored-By: Jonathan Kim <jkimbo@gmail.com>

Co-authored-by: Jonathan Kim <jkimbo@gmail.com>
2020-04-06 09:58:55 +01:00
Roy Segall
63418666d9
Adding Django filter as an installed app (#899) 2020-04-06 09:15:35 +01:00
Jonathan Kim
0da06d4d54
Update stale.yml 2020-03-18 12:59:32 +00:00
Ülgen Sarıkavak
cf9f59071e
Update dev dependencies (#903) 2020-03-14 10:40:41 +00:00
Jonathan Kim
150008aae5
v2.9.0 2020-03-13 10:13:46 +00:00
Jonathan Kim
c8a56f8857
Allow string references in DjangoListField (#885)
* Allow passing string references to DjangoListField

* Refactor logic to work with string imports
2020-03-13 10:05:35 +00:00
Jonathan Kim
348fcf37a0
Detect format from output file path (#868)
* Detect format from output file path

* Fix tests

* Add test for exporting graphql file

* Add some documentation
2020-03-13 10:04:55 +00:00
Jonathan Kim
b8e598d66d
Add options to override how Django Choice fields are converted t… (#860)
* Add new setting to create unique enum names

* Add specific tests for name generation

* Add schema test

* Rename settings field

* Rename setting

* Add custom function setting

* Add documentation

* Use format instead of f strings

* Update graphene_django/converter.py

Co-Authored-By: Syrus Akbary <me@syrusakbary.com>

* Fix tests

* Update docs

* Import function through import_string function

Co-authored-by: Syrus Akbary <me@syrusakbary.com>
2020-03-13 10:04:25 +00:00
Akhil Gopi
13352216a4
Improv/documentation fixes (#895)
* Bump up the minimum support Django version.

* Update documentation to mention support for Django 1.11.

Co-authored-by: Akhil <akhil@healthifyme.com>
2020-03-07 17:17:45 +01:00
Sergey Fursov
aeb04d5b5c
use actual root/variables/context args of the execute method (#878) 2020-02-23 09:50:40 +00:00
Sergey Protasov
235096362f
Update mutation.py (#811)
* Update mutation.py

* Add tests

Co-authored-by: Jonathan Kim <jkimbo@gmail.com>
2020-02-23 09:49:39 +00:00
Amim Knabben
a12fc9299a
Exclude read_only fields from input (#882) 2020-02-23 09:48:33 +00:00
Gabor Markowski
7a14b77a41
fix a typo in the warning (#884) 2020-02-23 09:47:41 +00:00
B4rtware
6a19ab5a4b
use to_representation in favor of get_attribute (#848)
* use `to_represenation` in favor of `get_attribute`

* fix datetime type does get converted to a string

to_representation will convert the datetime field into a string representation. However the to_representation on the method field will only call its underlying method.

* fix add missing import

* apply black formatter

* add test for serializer method field

* apply black format

* improve backward compatibility

by using date's class contructor instead of fromisostring

* apply black format

* fix black format issue
2020-02-21 17:42:47 +00:00
dependabot[bot]
d1a9444401
Bump django from 3.0 to 3.0.3 in /examples/cookbook (#876)
Bumps [django](https://github.com/django/django) from 3.0 to 3.0.3.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.0...3.0.3)

Signed-off-by: dependabot[bot] <support@github.com>
2020-02-21 17:40:34 +00:00
dependabot[bot]
b222caa901
Bump django from 3.0 to 3.0.3 in /examples/cookbook-plain (#877)
Bumps [django](https://github.com/django/django) from 3.0 to 3.0.3.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.0...3.0.3)

Signed-off-by: dependabot[bot] <support@github.com>
2020-02-21 17:40:18 +00:00
Jonathan Kim
11dafd4c48
v2.8.2 2020-02-17 11:09:11 +00:00
Jonathan Kim
4e1b82a8d8
Check exclude fields correctly (#873) 2020-02-17 11:08:44 +00:00
Manuel Bojato
bbf119cd3b Add anacounda cloud badge to graphene-django (#874) 2020-02-08 20:28:50 +00:00
Leeward Bound
83bc32bc9d
Adding support for disabling enum creation on SerializerMutation (#851) 2020-02-07 10:17:56 +00:00
Ben Howes
6b8c5bdefc
Allow for easier template overrides in graphiql (#863)
* don't replace <body>

* Update graphene_django/templates/graphene/graphiql.html

Co-Authored-By: Jonathan Kim <jkimbo@gmail.com>

* Fix editor styling and initialisation

Co-authored-by: Jonathan Kim <jkimbo@gmail.com>
2020-02-07 10:16:11 +00:00
Jonathan Kim
f3f0608606
v2.8.1 2020-02-07 09:59:05 +00:00
Jonathan Kim
280b38f804
Only warn if a field doesn't exist on the Django model (#862)
* Only warn if a field doesn't exist on the Django model

Also don't warn if the field name matches a custom field.

* Expand warning messages
2020-02-07 09:55:38 +00:00
Jason Kraus
1310509fa1
feature(stalebot): bug, documentation, help wanted, and enhancement added to exempt labels (#869) 2020-02-05 22:16:51 +00:00
Jonathan Kim
5c3199883f
Fix dependencies for examples (#861) 2020-01-31 14:20:18 +00:00
Ilya Zhelyabuzhsky
8ec456285b
Fix force_str deprecation warning (#858) 2020-01-29 10:06:38 +00:00
luto
62ecbae614 resolve django encoding deprecation warnings (#853)
https://docs.djangoproject.com/en/3.0/ref/utils/#django.utils.encoding.force_text
2020-01-20 21:05:20 +00:00
luto
08f67797d8 resolve django translation deprecation warnings (#847)
https://docs.djangoproject.com/en/3.0/releases/3.0/#id3
2020-01-11 14:52:41 +01:00
Jonathan Kim
96c38b4349
Update Django model form tests (#839)
* Clean up code and raise an exception if the model type is not found

* Update tests

* Fix tests
2020-01-11 14:49:44 +01:00
Jonathan Kim
de87573e0c
Add information on how to deal with CSRF protection (#838) 2020-01-11 14:49:17 +01:00
Jonathan Kim
b8a2d5953a Don't run tests during deploy stage 2019-12-31 14:34:47 +00:00
Jonathan Kim
399ad13a70
v2.8.0 2019-12-31 14:10:18 +00:00
Jonathan Kim
3dd04f68ab
Update travis config to only run deploy once (#837) 2019-12-31 13:56:04 +00:00
Vyacheslav Matyukhin
efe210f8ac Validate Meta.fields and Meta.exclude on DjangoObjectType (#842)
Resolves #840
2019-12-31 13:55:45 +00:00
Vyacheslav Matyukhin
f661cf8335 Fix typo in exclude type checking test (#841) 2019-12-30 14:14:41 +00:00
dan-klasson
7940a7b954 added support for partial updates in serializers (#731)
* added support for partial updates in serializers

* Add test to verify partial updates

Co-authored-by: Jonathan Kim <jkimbo@gmail.com>
2019-12-27 14:46:48 +00:00
cbergmiller
45df7445f4 Read csrftoken from DOM if no cookie is set (#826) 2019-12-27 14:26:42 +00:00
dependabot[bot]
3d01acf169 Bump django from 2.2.8 to 3.0 in /examples/cookbook (#825)
Bumps [django](https://github.com/django/django) from 2.2.8 to 3.0.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/2.2.8...3.0)

Signed-off-by: dependabot[bot] <support@github.com>
2019-12-27 14:25:34 +00:00
Chibuotu Amadi
b66a3f3479 Add headers arg to GraphQLTestCase.query (#827)
* Add headers arg to GraphQLTestCase.query

* fix headers NoneType case in GraphQLTestCase.query

* Run format

Co-authored-by: Jonathan Kim <jkimbo@gmail.com>
2019-12-26 11:45:18 +00:00
dependabot[bot]
968002f155 Bump django from 2.2.4 to 2.2.8 in /examples/cookbook (#821)
Bumps [django](https://github.com/django/django) from 2.2.4 to 2.2.8.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/2.2.4...2.2.8)

Signed-off-by: dependabot[bot] <support@github.com>
2019-12-07 19:56:57 +00:00
dependabot[bot]
a73d653274 Bump django from 2.2.4 to 2.2.8 in /examples/cookbook-plain (#822)
Bumps [django](https://github.com/django/django) from 2.2.4 to 2.2.8.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/2.2.4...2.2.8)

Signed-off-by: dependabot[bot] <support@github.com>
2019-12-07 19:56:40 +00:00
Jonathan Kim
374d8a8a9e
v2.7.1 2019-11-29 09:13:36 +00:00
Thiago Bellini Ribeiro
7e7f18ee0e Keep original queryset on DjangoFilterConnectionField (#816)
* Keep original queryset on DjangoFilterConnectionField

The PR #796 broke DjangoFilterConnectionField making it always get the
raw queryset from the model to apply the filters in it.

This makes sure that the DjangoObjectType's .get_queryset is called,
keeping any filtering it might have made.

* Add regression test
2019-11-29 09:13:16 +00:00
Jonathan Kim
e82a2d75c6
v2.7.0 2019-11-28 19:23:31 +00:00
Jason Kraus
a818ec9017 replace merge_queryset with resolve_queryset pattern (#796)
* replace merge_queryset with resolve_queryset pattern

* skip double limit test

* Update graphene_django/fields.py

Co-Authored-By: Jonathan Kim <jkimbo@gmail.com>

* yank skipped test

* fix bad variable ref

* add test for annotations

* add test for using  queryset with django filters

* document ththat one should use defer instead of values with queysets and DjangoObjectTypes
2019-11-28 10:49:37 +00:00
Jason Kraus
3ce44908c9 django-filter: resolve field along with lookup expression to pro… (#805)
* django-filter: resolve field along with lookup expression to properly resolve field

* bring back django-filter with method test

* remove dangling comment

* refactor based on better knowledge of django-filters
2019-11-28 10:48:03 +00:00
Athul Cyriac Ajay
e51e60209a Updated Tutorial with Highlights (#801) 2019-10-31 19:31:31 -04:00
Brett Jackson
def6b15e5b Update schema introspection docs to show SCHEMA_INDENT option (#802)
* Update schema introspection docs to show indent settings

* fix whitespace
2019-10-19 20:33:33 +01:00
Misha K
b085b5922a add Django 3.0 to the test matrix (#793)
* add Django 3.0 to the test matrix

* fix six imports
2019-10-18 11:38:59 +01:00
Jonathan Kim
e17582e1a1
Update stale.yml 2019-10-18 10:12:03 +01:00
Jens Diemer
8d95596ffb Note that release information are on github release page (#790) 2019-10-01 14:59:52 +01:00
Jonathan Kim
5068ea05c3
v2.6.0 2019-09-22 21:17:44 +01:00
Jonathan Kim
e4cf59ecec
Handle isnull filters differently (#753)
* Handle isnull filters differently

* Change to rsplit
2019-09-22 21:14:59 +01:00
Jason Kraus
a64ba65bef convert DRF ChoiceField to Enum (#537)
* convert DRF ChoiceField to Enum, also impacts FilePathField

* Pep8 fixes

* DRF multiple choices field converts to list of enum

* apply black formatting
2019-09-22 21:13:12 +01:00
rishabh
cd73cab699 converter.py: Fix typo posgres->postgres (#765)
Fixes typo for HStoreField and RangeField
converters.
2019-09-22 21:10:21 +01:00
Mel van Londen
0962db5aa6 Pin higher version of graphene for proper graphql-core version r… (#768) 2019-09-22 21:09:57 +01:00
Gilly Ames
4f21750fc2 Upgrade graphiql version to fix history tool (#772)
Graphiql has a history tool that allows you to save and label favourites, but this version has a bug (fixed https://github.com/graphql/graphiql/issues/750). This change upgrades to the latest version.
2019-09-22 20:43:46 +01:00
Jonathan Kim
fea9b5b194 Extend DjangoListField to use model queryset if none defined (#732)
* Fix model property

* Only allow DjangoObjectTypes to DjangoListField

* Resolve model queryset by default

* Add some more tests to check behaviour
2019-09-17 09:14:18 -07:00
Tyler Kennedy
4bbc0824a6 Fix a small typo, filerset_class -> filterset_class (#762) 2019-09-17 09:13:47 -07:00
Talley Lambert
254e59c36f Adds variables arg to GraphQLTestCase.query (#699)
* add variables arg in GraphQLTestCase.query

* update GraphQLTestCase.query docstring and remove type check
2019-09-07 11:49:29 -07:00
Semyon Pupkov
ac79b38cf0 Use field and exclude in docs instead deprecated attrs (#740) 2019-09-07 09:49:41 -07:00
A C SREEDHAR REDDY
1b8184ece1 make Mutation class ObjectType. (#748) 2019-08-16 14:34:28 +01:00
A C SREEDHAR REDDY
9d245287a4 is_authenticated is bool not callable. (#749) 2019-08-16 14:33:59 +01:00
Gert Van Gool
d5e71bc9be Fix typo of imoprt to import (#742) 2019-08-10 21:30:17 +01:00
dependabot[bot]
a04fff9d70 Bump django from 2.1.11 to 2.2.4 in /examples/cookbook-plain (#736)
Bumps [django](https://github.com/django/django) from 2.1.11 to 2.2.4.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/2.1.11...2.2.4)

Signed-off-by: dependabot[bot] <support@github.com>
2019-08-10 18:50:39 +01:00
Jonathan Kim
87aebdb630
v2.5.0 (#739) 2019-08-10 11:55:42 +01:00
dependabot[bot]
930adb50ce Bump django from 2.1.10 to 2.1.11 in /examples/cookbook-plain (#733)
Bumps [django](https://github.com/django/django) from 2.1.10 to 2.1.11.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/2.1.10...2.1.11)

Signed-off-by: dependabot[bot] <support@github.com>
2019-08-07 08:09:56 +01:00
dependabot[bot]
c432d5875b Bump django from 2.2.3 to 2.2.4 in /examples/cookbook (#734)
Bumps [django](https://github.com/django/django) from 2.2.3 to 2.2.4.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/2.2.3...2.2.4)

Signed-off-by: dependabot[bot] <support@github.com>
2019-08-07 08:09:42 +01:00
Tomasz Kontusz
11605dcdc6 Make DjangoDebugContext wait for nested fields (#591)
* Make DjangoDebugContext wait for nested fields

This commit makes DjangoDebugContext wait for all field's promises,
even for fields that only started their resolvers after __debug was
resolved.

Fixes #293.

* Run format
2019-08-07 08:09:17 +01:00
Kike Isidoro
6e137da469 Check for filters defined on base filterset classes (#730)
* Check for filters defined on base filterset classes

* Make python2.7 compatible and run black

* Add filter method and use filter in test

* Check article headline and reformat
2019-08-07 08:04:04 +01:00
Alexandre Kirszenberg
59f4f134b5 Set converted Django connections to required (#610) 2019-08-01 09:31:18 -07:00
Jason Kraus
b1a9293016 fix choices enum: if field can be blank then it isnt required (#714) 2019-08-01 09:07:52 +01:00
Jonathan Kim
51adb3632b
Update readme with Django path (#720) 2019-07-27 16:14:34 +02:00
Jonathan Kim
de98fb5812
v2.4.0 (#706) 2019-07-12 17:38:26 +01:00
Semyon Pupkov
224725039b Asserting status code before decoding json in assertResponseNoEr… (#708) 2019-07-11 20:32:07 +01:00
Jonathan Kim
b7e4937775
Alias only_fields as fields and exclude_fields as exclude (#691)
* Create new fields and exclude options that are aliased to exclude_fields and only_fields

* Update docs

* Add some checking around fields and exclude definitions

* Add all fields option

* Update docs to include `__all__` option

* Actual order of fields is not stable

* Update docs/queries.rst

Co-Authored-By: Semyon Pupkov <semen.pupkov@gmail.com>

* Fix example code

* Format code

* Start raising PendingDeprecationWarnings for using only_fields and exclude_fields

* Update tests
2019-07-09 14:03:11 +01:00
Pablo Burgos
a2103c19f4 Fix error of multiple inputs with the same type. When using same serializer. (#530) 2019-07-09 09:14:04 +01:00
Jonathan Kim
0988e0798a
Adds documentation to CAMELCASE_ERRORS setting (#689)
* Rename setting and add documentation

* Add examples

* Use `cls`
2019-07-08 22:22:08 +01:00
Jonathan Kim
aa30750d39 Bugfix: Correct filter types for DjangoFilterConnectionFields (#682)
* Get form field from Django model before defaulting to django-filter

* Add test

* Cleanup some flake8 warnings and pytest warnings

* Run isort and add black compatible config
2019-07-07 12:11:27 -07:00
Jonathan Kim
9aabe2cbe6
Remove duplicate ErrorType (#701) 2019-07-07 20:06:01 +01:00
dependabot[bot]
3b541e3d05 Bump django from 2.2.2 to 2.2.3 in /examples/cookbook (#694)
Bumps [django](https://github.com/django/django) from 2.2.2 to 2.2.3.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/2.2.2...2.2.3)

Signed-off-by: dependabot[bot] <support@github.com>
2019-07-04 10:26:54 +01:00
dependabot[bot]
470fb60dc5 Bump django from 2.1.9 to 2.1.10 in /examples/cookbook-plain (#695)
Bumps [django](https://github.com/django/django) from 2.1.9 to 2.1.10.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/2.1.9...2.1.10)

Signed-off-by: dependabot[bot] <support@github.com>
2019-07-04 10:26:27 +01:00
Jonathan Kim
40ae7e53ec
Fix manager check in DjangoConnectionField (#693)
* Fix default manager check

* Add test
2019-07-02 19:37:50 +01:00
Jonathan Kim
54cc6a4b13
Enforce NonNull for returned related Sets and their content (#690)
* Enforce NonNull for returned related Sets and their content. https://github.com/graphql-python/graphene-django/issues/448

* Run format.

* Remove duplicate assertion
2019-06-25 16:30:30 +01:00
Konstantin Alekseev
e2e496f505 Apply camel case converter to field names in DRF errors (#514)
* Apply camel case converter to field names in DRF errors

* Implement recursive error camelize, add setting.
2019-06-25 09:40:29 +01:00
Jonathan Kim
692540cc78
Update flake8 (#688)
* Include setup.py in black formatting

* Add new flake8 plugins and update errors to look for

* Fix duplicate test name

* Don't use mutable data structure

* Install all dev dependencies for flake8 and black tox envs
2019-06-24 18:55:44 +01:00
Semyon Pupkov
91c1278d1a Make cookbook example working on django 2 (#680) 2019-06-19 11:59:19 +01:00
Jonathan Kim
612ba5a4ea
Add convert_choices_to_enum option on DjangoObjectType Meta class (#674)
* Add convert_choices_to_enum meta option

* Add tests

* Run black

* Update documentation

* Add link to Django choices documentation

* Add test and documentation note

That setting to an empty list is the same as setting the value as False

* Fix Django warning in tests

* rst is not markdown
2019-06-17 18:48:29 +01:00
dependabot[bot]
894b1053a2 Bump django from 2.1.6 to 2.1.9 in /examples/cookbook-plain (#669)
Bumps [django](https://github.com/django/django) from 2.1.6 to 2.1.9.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/2.1.6...2.1.9)

Signed-off-by: dependabot[bot] <support@github.com>
2019-06-17 18:48:15 +01:00
dependabot[bot]
6169346776 Bump django from 1.11.20 to 1.11.21 in /examples/cookbook (#670)
Bumps [django](https://github.com/django/django) from 1.11.20 to 1.11.21.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/1.11.20...1.11.21)

Signed-off-by: dependabot[bot] <support@github.com>
2019-06-17 17:08:51 +01:00
Jonathan Kim
6e8dce95ae
Update doc setup (#673)
* Expose doc commands in root makefile and add autobuild

* Fix some errors

* Alias some commands and add PHONY
2019-06-14 12:33:37 +01:00
Jonathan Kim
775d2e3523
Update travis and tox (#667)
* Update travis and tox

* Use xenial distribution

* Don't install coveralls twice

* Add black and flake8 tox commands

* Remove Python 3.5 test for Django master

* Fix indent

* Ignore migrations

* Remove black for now

* Run black formatting (#668)

* Run black format

* Update makefile

* Add black to travis build
2019-06-10 20:54:30 -07:00
Mel van Londen
44e9b0d0c5 Add stale bot (#661) 2019-06-10 11:08:41 -07:00
Alexandre Kirszenberg
96934c4614 Correctly propagate help_text as description for many-to-* relations (#579)
* Correctly propagate help_text as description for many-to-* relations

* Trigger build
2019-06-09 17:19:05 -07:00
Emil Goldsmith Olesen
fcc491fffb Add watch option to graphql_schema (#656)
* Add watch option to graphql_schema

* add documentation for grapql_schema --watch
2019-06-09 17:06:50 -07:00
Mel van Londen
a9a8d672e9
Merge pull request #600 from sierreis/filterset-class
Add support for filterset_class meta parameter
2019-06-09 16:48:46 -07:00
Mel van Londen
b271b259bd
Merge pull request #603 from abettke/fix/enhanced-proxy-model-support
Adds enhanced support for proxy models.
2019-06-09 16:47:10 -07:00
Mel van Londen
83cc0d793b
Merge pull request #436 from cloudspectatordevelopment/tox
Added tox.ini and improved travis.yml
2019-06-09 16:45:28 -07:00
Abraham Toriz Cruz
f617b2a9c2 django 1.11.19 is not available, probably for security reasons (#652) 2019-06-09 16:33:57 -07:00
Emil Goldsmith Olesen
3cde872e28 Stop enforcing csrf checks in GraphQLTestCase (#658) 2019-06-09 16:30:48 -07:00
kamilkijak
c90c27f364 Add support for write_only fields in SerializerMutation (#555) 2019-06-09 16:25:34 -07:00
Anthony Monthe
cb9eed6765 Added tox.ini
Updated Travis YML
2019-05-22 01:49:52 +01:00
Andrew Bettke
a7ee042e9d Merge branch 'master' of https://github.com/graphql-python/graphene-django into fix/enhanced-proxy-model-support 2019-04-01 22:15:16 +13:00
Andrew Bettke
959e98eeb0 Refactor to use formal to_global_id. 2019-03-28 09:56:10 +13:00
sierreis
d2f8bf730b Test exception when both filterset_class and filter_fields are set 2019-03-27 14:05:42 -04:00
Andrew Bettke
a461e80ee4 Correctly encode / decode for python3+. 2019-03-27 17:56:06 +13:00
Andrew Bettke
83a2ad34cd Encode strings before passing to b64encode. 2019-03-27 17:28:56 +13:00
Andrew Bettke
980142dfcf Fix linting. 2019-03-27 17:24:13 +13:00
Andrew Bettke
36ac5626e9 Adds enhanced support for proxy models. 2019-03-27 17:09:25 +13:00
sierreis
132c4cb9d4 Fixed so that GrapheneFilterSetMixin is used with any provided filterset_class 2019-03-25 23:45:14 -04:00
sierreis
4d905a46ac Fixed flake8 lint error 2019-03-25 10:03:54 -04:00
sierreis
5c191b9062 Add support for filterset_class meta parameter
* Allow for use of either filter_fields or filterset_class
* Add tests to check that the behavior is similar to filter_fields
* Add documentation to show how to make use of the parameter
2019-03-25 00:38:49 -04:00
180 changed files with 12467 additions and 2307 deletions

34
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,34 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: "\U0001F41Bbug"
assignees: ''
---
**Note: for support questions, please use stackoverflow**. This repository's issues are reserved for feature requests and bug reports.
* **What is the current behavior?**
* **If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem** via
a github repo, https://repl.it or similar (you can use this template as a starting point: https://repl.it/@jkimbo/Graphene-Django-Example).
* **What is the expected behavior?**
* **What is the motivation / use case for changing the behavior?**
* **Please tell us about your environment:**
- Version:
- Platform:
* **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow)

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1 @@
blank_issues_enabled: false

View File

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: "✨enhancement"
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

22
.github/stale.yml vendored Normal file
View File

@ -0,0 +1,22 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: false
# Number of days of inactivity before a stale issue is closed
daysUntilClose: false
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
- 🐛bug
- 📖 documentation
- help wanted
- ✨enhancement
# Label to use when marking an issue as stale
staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: false
# markComment: >
# This issue has been automatically marked as stale because it has not had
# recent activity. It will be closed if no further activity occurs. Thank you
# for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

31
.github/workflows/deploy.yml vendored Normal file
View File

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

26
.github/workflows/lint.yml vendored Normal file
View File

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

43
.github/workflows/tests.yml vendored Normal file
View File

@ -0,0 +1,43 @@
name: Tests
on:
push:
branches: ["main"]
pull_request:
workflow_call:
jobs:
build:
runs-on: ubuntu-latest
strategy:
max-parallel: 4
matrix:
django: ["3.2", "4.2", "5.0", "5.1"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
exclude:
- django: "3.2"
python-version: "3.11"
- django: "3.2"
python-version: "3.12"
- django: "5.0"
python-version: "3.8"
- django: "5.0"
python-version: "3.9"
- django: "5.1"
python-version: "3.8"
- django: "5.1"
python-version: "3.9"
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox tox-gh-actions
- name: Test with tox
run: tox
env:
DJANGO: ${{ matrix.django }}

10
.gitignore vendored
View File

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

23
.pre-commit-config.yaml Normal file
View File

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

18
.readthedocs.yaml Normal file
View File

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

32
.ruff.toml Normal file
View File

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

View File

@ -1,67 +0,0 @@
language: python
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==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==3.7.7
fi
script:
- |
if [ "$TEST_TYPE" = lint ]; then
echo "Checking Python code lint."
flake8 graphene_django
exit
elif [ "$TEST_TYPE" = build ]; then
py.test --cov=graphene_django graphene_django examples
fi
after_success:
- |
if [ "$TEST_TYPE" = build ]; then
coveralls
fi
env:
matrix:
- TEST_TYPE=build DJANGO_VERSION=1.11
matrix:
fast_finish: true
include:
- python: '3.4'
env: TEST_TYPE=build DJANGO_VERSION=2.0
- python: '3.5'
env: TEST_TYPE=build DJANGO_VERSION=2.0
- python: '3.6'
env: TEST_TYPE=build DJANGO_VERSION=2.0
- python: '3.5'
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
on:
tags: true
password:
secure: kymIFCEPUbkgRqe2NAXkWfxMmGRfWvWBOP6LIXdVdkOOkm91fU7bndPGrAjos+/7gN0Org609ZmHSlVXNMJUWcsL2or/x5LcADJ4cZDe+79qynuoRb9xs1Ri4O4SBAuVMZxuVJvs8oUzT2R11ql5vASSMtXgbX+ZDGpmPRVZStkCuXgOc4LBhbPKyl3OFy7UQFPgAEmy3Yjh4ZSKzlXheK+S6mmr60+DCIjpaA0BWPxYK9FUE0qm7JJbHLUbwsUP/QMp5MmGjwFisXCNsIe686B7QKRaiOw62eJc2R7He8AuEC8T9OM4kRwDlecSn8mMpkoSB7QWtlJ+6XdLrJFPNvtrOfgfzS9/96Qrw9WlOslk68hMlhJeRb0s2YUD8tiV3UUkvbL1mfFoS4SI9U+rojS55KhUEJWHg1w7DjoOPoZmaIL2ChRupmvrFYNAGae1cxwG3Urh+t3wYlN3gpKsRDe5GOT7Wm2tr0ad3McCpDGUwSChX59BAJXe/MoLxkKScTrMyR8yMxHOF0b4zpVn5l7xB/o2Ik4zavx5q/0rGBMK2D+5d+gpQogKShoquTPsZUwO7sB5hYeH2hqGqpeGzZtb76E2zZYd18pJ0FsBudm5+KWjYdZ+vbtGrLxdTXJ1EEtzVXm0lscykTpqUucbXSa51dhStJvW2xEEz6p3rHo=
distributions: "sdist bdist_wheel"

View File

@ -33,7 +33,7 @@ make tests
## Opening Pull Requests
Please fork the project and open a pull request against the master branch.
Please fork the project and open a pull request against the `main` branch.
This will trigger a series of test and lint checks.
@ -59,4 +59,4 @@ Then to produce a HTML version of the documentation:
```sh
make html
```
```

View File

@ -1,3 +1,6 @@
include README.md LICENSE
recursive-include graphene_django/templates *
recursive-include graphene_django/static *
include examples/cookbook/cookbook/ingredients/fixtures/ingredients.json
include examples/cookbook-plain/cookbook/ingredients/fixtures/ingredients.json

View File

@ -1,11 +1,29 @@
.PHONY: help
help:
@echo "Please use \`make <target>' where <target> is one of"
@grep -E '^\.PHONY: [a-zA-Z_-]+ .*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = "(: |##)"}; {printf "\033[36m%-30s\033[0m %s\n", $$2, $$3}'
.PHONY: dev-setup ## Install development dependencies
dev-setup:
pip install -e ".[dev]"
python -m pre_commit install
.PHONY: tests ## Run unit tests
tests:
py.test graphene_django --cov=graphene_django -vv
PYTHONPATH=. pytest graphene_django --cov=graphene_django -vv
.PHONY: format ## Format code
format:
black graphene_django
ruff format graphene_django examples setup.py
.PHONY: lint ## Lint code
lint:
flake8 graphene_django
ruff graphene_django examples
.PHONY: docs ## Generate docs
docs: dev-setup
cd docs && make install && make html
.PHONY: docs-live ## Generate docs with live reloading
docs-live: dev-setup
cd docs && make install && make livehtml

172
README.md
View File

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

View File

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

View File

@ -1,35 +0,0 @@
import sys
import os
ROOT_PATH = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, ROOT_PATH + '/examples/')
SECRET_KEY = 1
INSTALLED_APPS = [
'graphene_django',
'graphene_django.rest_framework',
'graphene_django.tests',
'starwars',
]
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'django_test.sqlite',
}
}
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
},
]
GRAPHENE = {
'SCHEMA': 'graphene_django.tests.schema_view.schema'
}
ROOT_URLCONF = 'graphene_django.tests.urls'

View File

@ -48,12 +48,20 @@ help:
clean:
rm -rf $(BUILDDIR)/*
.PHONY: install ## to install all documentation related requirements
install:
pip install -r requirements.txt
.PHONY: html
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
.PHONY: livehtml ## to build and serve live-reloading documentation
livehtml:
sphinx-autobuild -b html --watch ../graphene_django $(ALLSPHINXOPTS) $(BUILDDIR)/html
.PHONY: dirhtml
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml

0
docs/_static/.gitkeep vendored Normal file
View File

View File

@ -20,7 +20,7 @@ Let's use a simple example model.
Limiting Field Access
---------------------
To limit fields in a GraphQL query simply use the ``only_fields`` meta attribute.
To limit fields in a GraphQL query simply use the ``fields`` meta attribute.
.. code:: python
@ -31,10 +31,10 @@ To limit fields in a GraphQL query simply use the ``only_fields`` meta attribute
class PostNode(DjangoObjectType):
class Meta:
model = Post
only_fields = ('title', 'content')
fields = ('title', 'content')
interfaces = (relay.Node, )
conversely you can use ``exclude_fields`` meta attribute.
conversely you can use ``exclude`` meta attribute.
.. code:: python
@ -45,9 +45,34 @@ conversely you can use ``exclude_fields`` meta attribute.
class PostNode(DjangoObjectType):
class Meta:
model = Post
exclude_fields = ('published', 'owner')
exclude = ('published', 'owner')
interfaces = (relay.Node, )
Another pattern is to have a resolve method act as a gatekeeper, returning None
or raising an exception if the client isn't allowed to see the data.
.. code:: python
from graphene import relay
from graphene_django.types import DjangoObjectType
from .models import Post
class PostNode(DjangoObjectType):
class Meta:
model = Post
fields = ('title', 'content', 'owner')
interfaces = (relay.Node, )
def resolve_owner(self, info):
user = info.context.user
if user.is_anonymous:
raise PermissionDenied("Please login")
if not user.is_staff:
return None
return self.owner
Queryset Filtering On Lists
---------------------------
@ -84,7 +109,7 @@ with the context argument.
def resolve_my_posts(self, info):
# context will reference to the Django request
if not info.context.user.is_authenticated():
if not info.context.user.is_authenticated:
return Post.objects.none()
else:
return Post.objects.filter(owner=info.context.user)
@ -111,6 +136,7 @@ If you are using ``DjangoObjectType`` you can define a custom `get_queryset`.
class PostNode(DjangoObjectType):
class Meta:
model = Post
fields = '__all__'
@classmethod
def get_queryset(cls, queryset, info):
@ -118,6 +144,21 @@ If you are using ``DjangoObjectType`` you can define a custom `get_queryset`.
return queryset.filter(published=True)
return queryset
.. warning::
Defining a custom ``get_queryset`` gives the guaranteed it will be called
when resolving the ``DjangoObjectType``, even through related objects.
Note that because of this, benefits from using ``select_related``
in objects that define a relation to this ``DjangoObjectType`` will be canceled out.
In the case of ``prefetch_related``, the benefits of the optimization will be lost only
if the custom ``get_queryset`` modifies the queryset. For more information about this, refers
to Django documentation about ``prefetch_related``: https://docs.djangoproject.com/en/4.2/ref/models/querysets/#prefetch-related.
If you want to explicitly disable the execution of the custom ``get_queryset`` when resolving,
you can decorate the resolver with `@graphene_django.bypass_get_queryset`. Note that this
can lead to authorization leaks if you are performing authorization checks in the custom
``get_queryset``.
Filtering ID-based Node Access
------------------------------
@ -133,7 +174,7 @@ method to your ``DjangoObjectType``.
class PostNode(DjangoObjectType):
class Meta:
model = Post
only_fields = ('title', 'content')
fields = ('title', 'content')
interfaces = (relay.Node, )
@classmethod
@ -154,7 +195,8 @@ Adding Login Required
To restrict users from accessing the GraphQL API page the standard Django LoginRequiredMixin_ can be used to create your own standard Django Class Based View, which includes the ``LoginRequiredMixin`` and subclasses the ``GraphQLView``.:
.. code:: python
#views.py
# views.py
from django.contrib.auth.mixins import LoginRequiredMixin
from graphene_django.views import GraphQLView
@ -165,22 +207,13 @@ To restrict users from accessing the GraphQL API page the standard Django LoginR
After this, you can use the new ``PrivateGraphQLView`` in the project's URL Configuration file ``url.py``:
For Django 1.9 and below:
For Django 2.2 and above:
.. code:: python
urlpatterns = [
# some other urls
url(r'^graphql$', PrivateGraphQLView.as_view(graphiql=True, schema=schema)),
# some other urls
path('graphql/', PrivateGraphQLView.as_view(graphiql=True, schema=schema)),
]
For Django 2.0 and above:
.. code:: python
urlpatterns = [
# some other urls
path('graphql', PrivateGraphQLView.as_view(graphiql=True, schema=schema)),
]
.. _LoginRequiredMixin: https://docs.djangoproject.com/en/1.10/topics/auth/default/#the-loginrequired-mixin
.. _LoginRequiredMixin: https://docs.djangoproject.com/en/dev/topics/auth/default/#the-loginrequired-mixin

View File

@ -60,25 +60,25 @@ source_suffix = ".rst"
master_doc = "index"
# General information about the project.
project = u"Graphene Django"
copyright = u"Graphene 2017"
author = u"Syrus Akbary"
project = "Graphene Django"
copyright = "Graphene 2017"
author = "Syrus Akbary"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = u"1.0"
version = "1.0"
# The full version, including alpha/beta/rc tags.
release = u"1.0.dev"
release = "1.0.dev"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
@ -276,7 +276,7 @@ latex_elements = {
# (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", "Graphene Documentation", "Syrus Akbary", "manual")
]
# The name of an image file (relative to this directory) to place at the top of
@ -317,7 +317,7 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, "graphene_django", u"Graphene Django Documentation", [author], 1)
(master_doc, "graphene_django", "Graphene Django Documentation", [author], 1)
]
# If true, show URL addresses after external links.
@ -334,7 +334,7 @@ texinfo_documents = [
(
master_doc,
"Graphene-Django",
u"Graphene Django Documentation",
"Graphene Django Documentation",
author,
"Graphene Django",
"One line description of project.",
@ -445,4 +445,7 @@ 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,
"python": ("https://docs.python.org/", None),
}

View File

@ -3,8 +3,8 @@ Django Debug Middleware
You can debug your GraphQL queries in a similar way to
`django-debug-toolbar <https://django-debug-toolbar.readthedocs.org/>`__,
but outputing in the results in GraphQL response as fields, instead of
the graphical HTML interface.
but outputting in the results in GraphQL response as fields, instead of
the graphical HTML interface. Exceptions with their stack traces are also exposed.
For that, you will need to add the plugin in your graphene schema.
@ -43,7 +43,7 @@ And in your ``settings.py``:
Querying
--------
You can query it for outputing all the sql transactions that happened in
You can query it for outputting all the sql transactions that happened in
the GraphQL request, like:
.. code::
@ -63,6 +63,10 @@ the GraphQL request, like:
sql {
rawSql
}
exceptions {
message
stack
}
}
}

12
docs/extra-types.rst Normal file
View File

@ -0,0 +1,12 @@
Extra Types
===========
Here are some libraries that provide common types for Django specific fields.
GeoDjango
---------
Use the graphene-gis_ library to add GeoDjango types to your Schema.
.. _graphene-gis: https://github.com/EverWinter23/graphene-gis

85
docs/fields.rst Normal file
View File

@ -0,0 +1,85 @@
Fields
======
Graphene-Django provides some useful fields to help integrate Django with your GraphQL
Schema.
DjangoListField
---------------
``DjangoListField`` allows you to define a list of :ref:`DjangoObjectType<queries-objecttypes>`'s. By default it will resolve the default queryset of the Django model.
.. code:: python
from graphene import ObjectType, Schema
from graphene_django import DjangoListField
class RecipeType(DjangoObjectType):
class Meta:
model = Recipe
fields = ("title", "instructions")
class Query(ObjectType):
recipes = DjangoListField(RecipeType)
schema = Schema(query=Query)
The above code results in the following schema definition:
.. code::
schema {
query: Query
}
type Query {
recipes: [RecipeType!]
}
type RecipeType {
title: String!
instructions: String!
}
Custom resolvers
****************
If your ``DjangoObjectType`` has defined a custom
:ref:`get_queryset<django-objecttype-get-queryset>` method, when resolving a
``DjangoListField`` it will be called with either the return of the field
resolver (if one is defined) or the default queryset from the Django model.
For example the following schema will only resolve recipes which have been
published and have a title:
.. code:: python
from graphene import ObjectType, Schema
from graphene_django import DjangoListField
class RecipeType(DjangoObjectType):
class Meta:
model = Recipe
fields = ("title", "instructions")
@classmethod
def get_queryset(cls, queryset, info):
# Filter out recipes that have no title
return queryset.exclude(title__exact="")
class Query(ObjectType):
recipes = DjangoListField(RecipeType)
def resolve_recipes(parent, info):
# Only get recipes that have been published
return Recipe.objects.filter(published=True)
schema = Schema(query=Query)
DjangoConnectionField
---------------------
``DjangoConnectionField`` acts similarly to ``DjangoListField`` but returns a
paginated connection following the `relay spec <https://relay.dev/graphql/connections.htm>`__
The field supports the following arguments: `first`, `last`, `offset`, `after` & `before`.

View File

@ -2,9 +2,8 @@ Filtering
=========
Graphene integrates with
`django-filter <https://django-filter.readthedocs.io/en/master/>`__ (2.x for
Python 3 or 1.x for Python 2) to provide filtering of results. See the `usage
documentation <https://django-filter.readthedocs.io/en/master/guide/usage.html#the-filter>`__
`django-filter <https://django-filter.readthedocs.io/en/stable/>`__ to provide filtering of results.
See the `usage documentation <https://django-filter.readthedocs.io/en/stable/guide/usage.html#the-filter>`__
for details on the format for ``filter_fields``.
This filtering is automatically available when implementing a ``relay.Node``.
@ -14,11 +13,20 @@ You will need to install it manually, which can be done as follows:
.. code:: bash
# You'll need to django-filter
# You'll need to install django-filter
pip install django-filter>=2
After installing ``django-filter`` you'll need to add the application in the ``settings.py`` file:
.. code:: python
INSTALLED_APPS = [
# ...
"django_filters",
]
Note: The techniques below are demoed in the `cookbook example
app <https://github.com/graphql-python/graphene-django/tree/master/examples/cookbook>`__.
app <https://github.com/graphql-python/graphene-django/tree/main/examples/cookbook>`__.
Filterable fields
-----------------
@ -26,7 +34,7 @@ Filterable fields
The ``filter_fields`` parameter is used to specify the fields which can
be filtered upon. The value specified here is passed directly to
``django-filter``, so see the `filtering
documentation <https://django-filter.readthedocs.io/en/master/guide/usage.html#the-filter>`__
documentation <https://django-filter.readthedocs.io/en/main/guide/usage.html#the-filter>`__
for full details on the range of options available.
For example:
@ -37,6 +45,7 @@ For example:
class Meta:
# Assume you have an Animal model defined with the following fields
model = Animal
fields = '__all__'
filter_fields = ['name', 'genus', 'is_domesticated']
interfaces = (relay.Node, )
@ -67,6 +76,7 @@ You can also make more complex lookup types available:
class AnimalNode(DjangoObjectType):
class Meta:
model = Animal
fields = '__all__'
# Provide more complex lookup types
filter_fields = {
'name': ['exact', 'icontains', 'istartswith'],
@ -100,7 +110,7 @@ features of ``django-filter``. This is done by transparently creating a
``filter_fields``.
However, you may find this to be insufficient. In these cases you can
create your own ``Filterset`` as follows:
create your own ``FilterSet``. You can pass it directly as follows:
.. code:: python
@ -108,6 +118,7 @@ create your own ``Filterset`` as follows:
class Meta:
# Assume you have an Animal model defined with the following fields
model = Animal
fields = '__all__'
filter_fields = ['name', 'genus', 'is_domesticated']
interfaces = (relay.Node, )
@ -115,6 +126,15 @@ create your own ``Filterset`` as follows:
class AnimalFilter(django_filters.FilterSet):
# Do case-insensitive lookups on 'name'
name = django_filters.CharFilter(lookup_expr=['iexact'])
# Allow multiple genera to be selected at once
genera = django_filters.MultipleChoiceFilter(
field_name='genus',
choices=(
('Canis', 'Canis'),
('Panthera', 'Panthera'),
('Seahorse', 'Seahorse')
)
)
class Meta:
model = Animal
@ -127,7 +147,52 @@ create your own ``Filterset`` as follows:
all_animals = DjangoFilterConnectionField(AnimalNode,
filterset_class=AnimalFilter)
The context argument is passed on as the `request argument <http://django-filter.readthedocs.io/en/master/guide/usage.html#request-based-filtering>`__
If you were interested in selecting all dogs and cats, you might query as follows:
.. code::
query {
allAnimals(genera: ["Canis", "Panthera"]) {
edges {
node {
id,
name
}
}
}
}
You can also specify the ``FilterSet`` class using the ``filterset_class``
parameter when defining your ``DjangoObjectType``, however, this can't be used
in unison with the ``filter_fields`` parameter:
.. code:: python
class AnimalFilter(django_filters.FilterSet):
# Do case-insensitive lookups on 'name'
name = django_filters.CharFilter(lookup_expr=['iexact'])
class Meta:
# Assume you have an Animal model defined with the following fields
model = Animal
fields = ['name', 'genus', 'is_domesticated']
class AnimalNode(DjangoObjectType):
class Meta:
model = Animal
fields = '__all__'
filterset_class = AnimalFilter
interfaces = (relay.Node, )
class Query(ObjectType):
animal = relay.Node.Field(AnimalNode)
all_animals = DjangoFilterConnectionField(AnimalNode)
The context argument is passed on as the `request argument <http://django-filter.readthedocs.io/en/main/guide/usage.html#request-based-filtering>`__
in a ``django_filters.FilterSet`` instance. You can use this to customize your
filters to be context-dependent. We could modify the ``AnimalFilter`` above to
pre-filter animals owned by the authenticated user (set in ``context.user``).
@ -165,7 +230,7 @@ Extend the tuple of fields if you want to order by more than one field.
order_by = OrderingFilter(
fields=(
('created_at', 'created_at'),
('name', 'created_at'),
)
)
@ -175,6 +240,7 @@ Extend the tuple of fields if you want to order by more than one field.
class Meta:
name = 'Group'
model = GroupModel
fields = '__all__'
interfaces = (relay.Node,)
def resolve_users(self, info, **kwargs):
@ -191,4 +257,87 @@ with this set up, you can now order the users under group:
xxx
}
}
}
}
PostgreSQL `ArrayField`
-----------------------
Graphene provides an easy to implement filters on `ArrayField` as they are not natively supported by django_filters:
.. code:: python
from django.db import models
from django_filters import FilterSet, OrderingFilter
from graphene_django.filter import ArrayFilter
class Event(models.Model):
name = models.CharField(max_length=50)
tags = ArrayField(models.CharField(max_length=50))
class EventFilterSet(FilterSet):
class Meta:
model = Event
fields = {
"name": ["exact", "contains"],
}
tags__contains = ArrayFilter(field_name="tags", lookup_expr="contains")
tags__overlap = ArrayFilter(field_name="tags", lookup_expr="overlap")
tags = ArrayFilter(field_name="tags", lookup_expr="exact")
class EventType(DjangoObjectType):
class Meta:
model = Event
interfaces = (Node,)
fields = "__all__"
filterset_class = EventFilterSet
with this set up, you can now filter events by tags:
.. code::
query {
events(tags_Overlap: ["concert", "festival"]) {
name
}
}
`TypedFilter`
-------------
Sometimes the automatic detection of the filter input type is not satisfactory for what you are trying to achieve.
You can then explicitly specify the input type you want for your filter by using a `TypedFilter`:
.. code:: python
from django.db import models
from django_filters import FilterSet, OrderingFilter
import graphene
from graphene_django.filter import TypedFilter
class Event(models.Model):
name = models.CharField(max_length=50)
class EventFilterSet(FilterSet):
class Meta:
model = Event
fields = {
"name": ["exact", "contains"],
}
only_first = TypedFilter(input_type=graphene.Boolean, method="only_first_filter")
def only_first_filter(self, queryset, _name, value):
if value:
return queryset[:1]
else:
return queryset
class EventType(DjangoObjectType):
class Meta:
model = Event
interfaces = (Node,)
fields = "__all__"
filterset_class = EventFilterSet

View File

@ -9,8 +9,8 @@ Graphene-Django provides some additional abstractions that make it easy to add G
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 <https://docs.graphene-python.org/en/latest/>`__ to familiarize yourself with the basic utilities.
Core tenants
------------
Core tenets
-----------
If you want to expose your data through GraphQL - read the ``Installation``, ``Schema`` and ``Queries`` section.
@ -25,10 +25,14 @@ For more advanced use, check out the Relay tutorial.
tutorial-relay
schema
queries
fields
extra-types
mutations
subscriptions
filtering
authorization
debug
introspection
validation
testing
settings

View File

@ -8,7 +8,7 @@ Requirements
Graphene-Django currently supports the following versions of Django:
* Django 2.X
* >= Django 2.2
Installation
------------
@ -25,13 +25,15 @@ Add ``graphene_django`` to the ``INSTALLED_APPS`` in the ``settings.py`` file of
INSTALLED_APPS = [
...
'django.contrib.staticfiles', # Required for GraphiQL
'graphene_django'
"django.contrib.staticfiles", # Required for GraphiQL
"graphene_django"
]
We need to add a ``graphql`` URL to the ``urls.py`` of your Django project:
For Django 2.2 and above:
.. code:: python
from django.urls import path
@ -49,7 +51,7 @@ Finally, define the schema location for Graphene in the ``settings.py`` file of
.. code:: python
GRAPHENE = {
'SCHEMA': 'django_root.schema.schema'
"SCHEMA": "django_root.schema.schema"
}
Where ``path.schema.schema`` is the location of the ``Schema`` object in your Django project.
@ -61,9 +63,31 @@ The most basic ``schema.py`` looks like this:
import graphene
class Query(graphene.ObjectType):
pass
hello = graphene.String(default_value="Hi!")
schema = graphene.Schema(query=Query)
To learn how to extend the schema object for your project, read the basic tutorial.
To learn how to extend the schema object for your project, read the basic tutorial.
CSRF exempt
-----------
If you have enabled `CSRF protection <https://docs.djangoproject.com/en/3.0/ref/csrf/>`_ in your Django app
you will find that it prevents your API clients from POSTing to the ``graphql`` endpoint. You can either
update your API client to pass the CSRF token with each request (the Django docs have a guide on how to do that: https://docs.djangoproject.com/en/3.0/ref/csrf/#ajax) or you can exempt your Graphql endpoint from CSRF protection by wrapping the ``GraphQLView`` with the ``csrf_exempt``
decorator:
.. code:: python
# urls.py
from django.urls import path
from django.views.decorators.csrf import csrf_exempt
from graphene_django.views import GraphQLView
urlpatterns = [
# ...
path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True))),
]

View File

@ -29,20 +29,37 @@ you're ready to use Relay with Graphene GraphQL implementation.
The schema file is sorted to create a reproducible canonical representation.
GraphQL SDL Representation
--------------------------
The schema can also be exported as a GraphQL SDL file by changing the file
extension :
.. code:: bash
./manage.py graphql_schema --schema tutorial.quickstart.schema --out schema.graphql
When exporting the schema as a ``.graphql`` file the ``--indent`` option is
ignored.
Advanced Usage
--------------
The ``--indent`` option can be used to specify the number of indentation spaces to
be used in the output. Defaults to `None` which displays all data on a single line.
The ``--watch`` option can be used to run ``./manage.py graphql_schema`` in watch mode, where it will automatically output a new schema every time there are file changes in your project
To simplify the command to ``./manage.py graphql_schema``, you can
specify the parameters in your settings.py:
.. code:: python
GRAPHENE = {
'SCHEMA': 'tutorial.quickstart.schema',
'SCHEMA_OUTPUT': 'data/schema.json' # defaults to schema.json
'SCHEMA': 'tutorial.quickstart.schema',
'SCHEMA_OUTPUT': 'data/schema.json', # defaults to schema.json,
'SCHEMA_INDENT': 2, # Defaults to None (displays all data on a single line)
}

View File

@ -25,6 +25,7 @@ Simple example
class QuestionType(DjangoObjectType):
class Meta:
model = Question
fields = '__all__'
class QuestionMutation(graphene.Mutation):
@ -36,7 +37,8 @@ Simple example
# The class attributes define the response of the mutation
question = graphene.Field(QuestionType)
def mutate(self, info, text, id):
@classmethod
def mutate(cls, root, info, text, id):
question = Question.objects.get(pk=id)
question.text = text
question.save()
@ -44,7 +46,7 @@ Simple example
return QuestionMutation(question=question)
class Mutation:
class Mutation(graphene.ObjectType):
update_question = QuestionMutation.Field()
@ -90,6 +92,7 @@ DjangoModelFormMutation
class PetType(DjangoObjectType):
class Meta:
model = Pet
fields = '__all__'
class PetMutation(DjangoModelFormMutation):
pet = Field(PetType)
@ -122,6 +125,55 @@ to change how the form is saved or to return a different Graphene object type.
If the form is *not* valid then a list of errors will be returned. These errors have two fields: ``field``, a string
containing the name of the invalid form field, and ``messages``, a list of strings with the validation messages.
DjangoFormInputObjectType
~~~~~~~~~~~~~~~~~~~~~~~~~
``DjangoFormInputObjectType`` is used in mutations to create input fields by **using django form** to retrieve input data structure from it. This can be helpful in situations where you need to pass data to several django forms in one mutation.
.. code:: python
from graphene_django.forms.types import DjangoFormInputObjectType
class PetFormInput(DjangoFormInputObjectType):
# any other fields can be placed here as well as
# other djangoforminputobjects and intputobjects
class Meta:
form_class = PetForm
object_type = PetType
class QuestionFormInput(DjangoFormInputObjectType)
class Meta:
form_class = QuestionForm
object_type = QuestionType
class SeveralFormsInputData(graphene.InputObjectType):
pet = PetFormInput(required=True)
question = QuestionFormInput(required=True)
class SomeSophisticatedMutation(graphene.Mutation):
class Arguments:
data = SeveralFormsInputData(required=True)
@staticmethod
def mutate(_root, _info, data):
pet_form_inst = PetForm(data=data.pet)
question_form_inst = QuestionForm(data=data.question)
if pet_form_inst.is_valid():
pet_model_instance = pet_form_inst.save(commit=False)
if question_form_inst.is_valid():
question_model_instance = question_form_inst.save(commit=False)
# ...
Additional to **InputObjectType** ``Meta`` class attributes:
* ``form_class`` is required and should be equal to django form class.
* ``object_type`` is not required and used to enable convertion of enum values back to original if model object type ``convert_choices_to_enum`` ``Meta`` class attribute is not set to ``False``. Any data field, which have choices in django, with value ``A_1`` (for example) from client will be automatically converted to ``1`` in mutation data.
* ``add_id_field_name`` is used to specify `id` field name (not required, by default equal to ``id``)
* ``add_id_field_type`` is used to specify `id` field type (not required, default is ``graphene.ID``)
Django REST Framework
---------------------
@ -151,7 +203,7 @@ customize the look up with the ``lookup_field`` attribute on the ``SerializerMut
.. code:: python
from graphene_django.rest_framework.mutation import SerializerMutation
from .serializers imoprt MyModelSerializer
from .serializers import MyModelSerializer
class AwesomeModelMutation(SerializerMutation):
@ -168,7 +220,7 @@ 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
from .serializers import MyModelSerializer
class AwesomeModelMutation(SerializerMutation):
@ -199,7 +251,7 @@ You can use relay with mutations. A Relay mutation must inherit from
.. code:: python
import graphene
import graphene
from graphene import relay
from graphene_django import DjangoObjectType
from graphql_relay import from_global_id
@ -229,3 +281,121 @@ This argument is also sent back to the client with the mutation result
(you do not have to do anything). For services that manage
a pool of many GraphQL requests in bulk, the ``clientIDMutation``
allows you to match up a specific mutation with the response.
Django Database Transactions
----------------------------
Django gives you a few ways to control how database transactions are managed.
Tying transactions to HTTP requests
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A common way to handle transactions in Django is to wrap each request in a transaction.
Set ``ATOMIC_REQUESTS`` settings to ``True`` in the configuration of each database for
which you want to enable this behavior.
It works like this. Before calling ``GraphQLView`` Django starts a transaction. If the
response is produced without problems, Django commits the transaction. If the view, a
``DjangoFormMutation`` or a ``DjangoModelFormMutation`` produces an exception, Django
rolls back the transaction.
.. warning::
While the simplicity of this transaction model is appealing, it also makes it
inefficient when traffic increases. Opening a transaction for every request has some
overhead. The impact on performance depends on the query patterns of your application
and on how well your database handles locking.
Check the next section for a better solution.
Tying transactions to mutations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A mutation can contain multiple fields, just like a query. There's one important
distinction between queries and mutations, other than the name:
..
`While query fields are executed in parallel, mutation fields run in series, one
after the other.`
This means that if we send two ``incrementCredits`` mutations in one request, the first
is guaranteed to finish before the second begins, ensuring that we don't end up with a
race condition with ourselves.
On the other hand, if the first ``incrementCredits`` runs successfully but the second
one does not, the operation cannot be retried as it is. That's why is a good idea to
run all mutation fields in a transaction, to guarantee all occur or nothing occurs.
To enable this behavior for all databases set the graphene ``ATOMIC_MUTATIONS`` settings
to ``True`` in your settings file:
.. code:: python
GRAPHENE = {
# ...
"ATOMIC_MUTATIONS": True,
}
On the contrary, if you want to enable this behavior for a specific database, set
``ATOMIC_MUTATIONS`` to ``True`` in your database settings:
.. code:: python
DATABASES = {
"default": {
# ...
"ATOMIC_MUTATIONS": True,
},
# ...
}
Now, given the following example mutation:
.. code::
mutation IncreaseCreditsTwice {
increaseCredits1: increaseCredits(input: { amount: 10 }) {
balance
errors {
field
messages
}
}
increaseCredits2: increaseCredits(input: { amount: -1 }) {
balance
errors {
field
messages
}
}
}
The server is going to return something like:
.. code:: json
{
"data": {
"increaseCredits1": {
"balance": 10.0,
"errors": []
},
"increaseCredits2": {
"balance": null,
"errors": [
{
"field": "amount",
"message": "Amount should be a positive number"
}
]
},
}
}
But the balance will remain the same.

View File

@ -1,3 +1,5 @@
.. _queries-objecttypes:
Queries & ObjectTypes
=====================
@ -18,78 +20,187 @@ Full example
# my_app/schema.py
import graphene
from graphene_django import DjangoObjectType
from graphene_django.types import DjangoObjectType
from .models import Question
class QuestionType(DjangoObjectType):
class Meta:
model = Question
fields = ("id", "question_text")
class Query:
class Query(graphene.ObjectType):
questions = graphene.List(QuestionType)
question = graphene.Field(QuestionType, question_id=graphene.String())
question_by_id = graphene.Field(QuestionType, id=graphene.String())
def resolve_questions(self, info, **kwargs):
def resolve_questions(root, info, **kwargs):
# Querying a list
return Question.objects.all()
def resolve_question(self, info, question_id):
def resolve_question_by_id(root, info, id):
# Querying a single question
return Question.objects.get(pk=question_id)
return Question.objects.get(pk=id)
Fields
------
Specifying which fields to include
----------------------------------
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``.
If you only want a subset of fields to be present, you can do so using
``fields`` or ``exclude``. It is strongly recommended that you explicitly set
all fields that should be exposed using the fields attribute.
This will make it less likely to result in unintentionally exposing data when
your models change.
only_fields
~~~~~~~~~~~
Setting neither ``fields`` nor ``exclude`` is deprecated and will raise a warning, you should at least explicitly make
``DjangoObjectType`` include all fields in the model as described below.
``fields``
~~~~~~~~~~
Show **only** these fields on the model:
.. code:: python
from graphene_django import DjangoObjectType
from .models import Question
class QuestionType(DjangoObjectType):
class Meta:
model = Question
only_fields = ('question_text')
fields = ("id", "question_text")
You can also set the ``fields`` attribute to the special value ``"__all__"`` to indicate that all fields in the model should be used.
exclude_fields
~~~~~~~~~~~~~~
Show all fields **except** those in ``exclude_fields``:
For example:
.. code:: python
from graphene_django import DjangoObjectType
from .models import Question
class QuestionType(DjangoObjectType):
class Meta:
model = Question
exclude_fields = ('question_text')
fields = "__all__"
Customised fields
~~~~~~~~~~~~~~~~~
``exclude``
~~~~~~~~~~~
Show all fields **except** those in ``exclude``:
.. code:: python
from graphene_django import DjangoObjectType
from .models import Question
class QuestionType(DjangoObjectType):
class Meta:
model = Question
exclude = ("question_text",)
Customising fields
------------------
You can completely overwrite a field, or add new fields, to a ``DjangoObjectType`` using a Resolver:
.. code:: python
from graphene_django import DjangoObjectType
from .models import Question
class QuestionType(DjangoObjectType):
class Meta:
model = Question
exclude_fields = ('question_text')
fields = ("id", "question_text")
extra_field = graphene.String()
def resolve_extra_field(self, info):
return 'hello!'
return "hello!"
Choices to Enum conversion
~~~~~~~~~~~~~~~~~~~~~~~~~~
By default Graphene-Django will convert any Django fields that have `choices`_
defined into a GraphQL enum type.
.. _choices: https://docs.djangoproject.com/en/2.2/ref/models/fields/#choices
For example the following ``Model`` and ``DjangoObjectType``:
.. code:: python
from django.db import models
from graphene_django import DjangoObjectType
class PetModel(models.Model):
kind = models.CharField(
max_length=100,
choices=(("cat", "Cat"), ("dog", "Dog"))
)
class Pet(DjangoObjectType):
class Meta:
model = PetModel
fields = ("id", "kind",)
Results in the following GraphQL schema definition:
.. code:: graphql
type Pet {
id: ID!
kind: PetModelKind!
}
enum PetModelKind {
CAT
DOG
}
You can disable this automatic conversion by setting
``convert_choices_to_enum`` attribute to ``False`` on the ``DjangoObjectType``
``Meta`` class.
.. code:: python
from graphene_django import DjangoObjectType
from .models import PetModel
class Pet(DjangoObjectType):
class Meta:
model = PetModel
fields = ("id", "kind",)
convert_choices_to_enum = False
.. code:: graphql
type Pet {
id: ID!
kind: String!
}
You can also set ``convert_choices_to_enum`` to a list of fields that should be
automatically converted into enums:
.. code:: python
from graphene_django import DjangoObjectType
from .models import PetModel
class Pet(DjangoObjectType):
class Meta:
model = PetModel
fields = ("id", "kind",)
convert_choices_to_enum = ["kind"]
**Note:** Setting ``convert_choices_to_enum = []`` is the same as setting it to
``False``.
Related models
@ -99,6 +210,8 @@ Say you have the following models:
.. code:: python
from django.db import models
class Category(models.Model):
foo = models.CharField(max_length=256)
@ -110,10 +223,13 @@ When ``Question`` is published as a ``DjangoObjectType`` and you want to add ``C
.. code:: python
from graphene_django import DjangoObjectType
from .models import Question
class QuestionType(DjangoObjectType):
class Meta:
model = Question
only_fields = ('category',)
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
@ -121,9 +237,15 @@ need to create the most basic class for this to work:
.. code:: python
from graphene_django import DjangoObjectType
from .models import Category
class CategoryType(DjangoObjectType):
class Meta:
model = Category
fields = ("foo",)
.. _django-objecttype-get-queryset:
Default QuerySet
-----------------
@ -136,10 +258,10 @@ Use this to control filtering on the ObjectType level instead of the Query objec
from graphene_django.types import DjangoObjectType
from .models import Question
class QuestionType(DjangoObjectType):
class Meta:
model = Question
fields = "__all__"
@classmethod
def get_queryset(cls, queryset, info):
@ -156,18 +278,22 @@ This resolve method should follow this format:
.. code:: python
def resolve_foo(self, info, **kwargs):
def resolve_foo(parent, info, **kwargs):
Where "foo" is the name of the field declared in the ``Query`` object.
.. code:: python
class Query:
import graphene
from .models import Question
from .types import QuestionType
class Query(graphene.ObjectType):
foo = graphene.List(QuestionType)
def resolve_foo(self, info, **kwargs):
id = kwargs.get('id')
return QuestionModel.objects.get(id)
def resolve_foo(root, info, **kwargs):
id = kwargs.get("id")
return Question.objects.get(id)
Arguments
~~~~~~~~~
@ -176,10 +302,18 @@ Additionally, Resolvers will receive **any arguments declared in the field defin
.. code:: python
class Query:
question = graphene.Field(Question, foo=graphene.String(), bar=graphene.Int())
import graphene
from .models import Question
from .types import QuestionType
def resolve_question(self, info, foo, bar):
class Query(graphene.ObjectType):
question = graphene.Field(
QuestionType,
foo=graphene.String(),
bar=graphene.Int()
)
def resolve_question(root, info, foo=None, bar=None):
# If `foo` or `bar` are declared in the GraphQL query they will be here, else None.
return Question.objects.filter(foo=foo, bar=bar).first()
@ -194,12 +328,27 @@ of Django's ``HTTPRequest`` in your resolve methods, such as checking for authen
.. 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()
import graphene
from .models import Question
from .types import QuestionType
class Query(graphene.ObjectType):
questions = graphene.List(QuestionType)
def resolve_questions(root, info):
# See if a user is authenticated
if info.context.user.is_authenticated():
return Question.objects.all()
else:
return Question.objects.none()
DjangoObjectTypes
~~~~~~~~~~~~~~~~~
A Resolver that maps to a defined `DjangoObjectType` should only use methods that return a queryset.
Queryset methods like `values` will return dictionaries, use `defer` instead.
Plain ObjectTypes
@ -214,15 +363,13 @@ Django models and your external API.
import graphene
from .models import Question
class MyQuestion(graphene.ObjectType):
text = graphene.String()
class Query:
class Query(graphene.ObjectType):
question = graphene.Field(MyQuestion, question_id=graphene.String())
def resolve_question(self, info, question_id):
def resolve_question(root, info, question_id):
question = Question.objects.get(pk=question_id)
return MyQuestion(
text=question.question_text
@ -252,51 +399,48 @@ the core graphene pages for more information on customizing the Relay experience
from graphene_django import DjangoObjectType
from .models import Question
class QuestionType(DjangoObjectType):
class Meta:
model = Question
interfaces = (relay.Node,)
interfaces = (relay.Node,) # make sure you add this
fields = "__all__"
class QuestionConnection(relay.Connection):
class Meta:
node = QuestionType
class Query:
questions = relay.ConnectionField(QuestionConnection)
def resolve_questions(root, info, **kwargs):
return Question.objects.all()
You can now execute queries like:
.. code:: python
.. code:: graphql
{
questions (first: 2, after: "YXJyYXljb25uZWN0aW9uOjEwNQ==") {
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
startCursor
endCursor
hasNextPage
hasPreviousPage
}
edges {
cursor
node {
id
question_text
}
cursor
node {
id
question_text
}
}
}
}
Which returns:
.. code:: python
.. code:: json
{
"data": {

View File

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

57
docs/schema.py Normal file
View File

@ -0,0 +1,57 @@
import graphene
from graphene_django.types import DjangoObjectType
from cookbook.ingredients.models import Category, Ingredient
class CategoryType(DjangoObjectType):
class Meta:
model = Category
fields = "__all__"
class IngredientType(DjangoObjectType):
class Meta:
model = Ingredient
fields = "__all__"
class Query:
category = graphene.Field(CategoryType, id=graphene.Int(), name=graphene.String())
all_categories = graphene.List(CategoryType)
ingredient = graphene.Field(
IngredientType, id=graphene.Int(), name=graphene.String()
)
all_ingredients = graphene.List(IngredientType)
def resolve_all_categories(self, info, **kwargs):
return Category.objects.all()
def resolve_all_ingredients(self, info, **kwargs):
return Ingredient.objects.all()
def resolve_category(self, info, **kwargs):
id = kwargs.get("id")
name = kwargs.get("name")
if id is not None:
return Category.objects.get(pk=id)
if name is not None:
return Category.objects.get(name=name)
return None
def resolve_ingredient(self, info, **kwargs):
id = kwargs.get("id")
name = kwargs.get("name")
if id is not None:
return Ingredient.objects.get(pk=id)
if name is not None:
return Ingredient.objects.get(name=name)
return None

View File

@ -6,7 +6,7 @@ Graphene-Django can be customised using settings. This page explains each settin
Usage
-----
Add settings to your Django project by creating a Dictonary with name ``GRAPHENE`` in the project's ``settings.py``:
Add settings to your Django project by creating a Dictionary with name ``GRAPHENE`` in the project's ``settings.py``:
.. code:: python
@ -30,7 +30,7 @@ Default: ``None``
``SCHEMA_OUTPUT``
----------
-----------------
The name of the file where the GraphQL schema output will go.
@ -44,7 +44,7 @@ Default: ``schema.json``
``SCHEMA_INDENT``
----------
-----------------
The indentation level of the schema output.
@ -58,7 +58,7 @@ Default: ``2``
``MIDDLEWARE``
----------
--------------
A tuple of middleware that will be executed for each GraphQL query.
@ -76,7 +76,7 @@ Default: ``()``
``RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST``
----------
------------------------------------------
Enforces relay queries to have the ``first`` or ``last`` argument.
@ -90,7 +90,7 @@ Default: ``False``
``RELAY_CONNECTION_MAX_LIMIT``
----------
------------------------------
The maximum size of objects that can be requested through a relay connection.
@ -101,3 +101,191 @@ Default: ``100``
GRAPHENE = {
'RELAY_CONNECTION_MAX_LIMIT': 100,
}
``CAMELCASE_ERRORS``
--------------------
When set to ``True`` field names in the ``errors`` object will be camel case.
By default they will be snake case.
Default: ``False``
.. code:: python
GRAPHENE = {
'CAMELCASE_ERRORS': False,
}
# result = schema.execute(...)
print(result.errors)
# [
# {
# 'field': 'test_field',
# 'messages': ['This field is required.'],
# }
# ]
.. code:: python
GRAPHENE = {
'CAMELCASE_ERRORS': True,
}
# result = schema.execute(...)
print(result.errors)
# [
# {
# 'field': 'testField',
# 'messages': ['This field is required.'],
# }
# ]
``DJANGO_CHOICE_FIELD_ENUM_CONVERT``
--------------------------------------
When set to ``True`` Django choice fields are automatically converted into Enum types.
Can be disabled globally by setting it to ``False``.
Default: ``True``
``DJANGO_CHOICE_FIELD_ENUM_V2_NAMING``
--------------------------------------
Set to ``True`` to use the old naming format for the auto generated Enum types from Django choice fields. The old format looks like this: ``{object_name}_{field_name}``
Default: ``False``
``DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME``
----------------------------------------
Define the path of a function that takes the Django choice field and returns a string to completely customise the naming for the Enum type.
If set to a function then the ``DJANGO_CHOICE_FIELD_ENUM_V2_NAMING`` setting is ignored.
Default: ``None``
.. code:: python
# myapp.utils
def enum_naming(field):
if isinstance(field.model, User):
return f"CustomUserEnum{field.name.title()}"
return f"CustomEnum{field.name.title()}"
GRAPHENE = {
'DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME': "myapp.utils.enum_naming"
}
``SUBSCRIPTION_PATH``
---------------------
Define an alternative URL path where subscription operations should be routed.
The GraphiQL interface will use this setting to intelligently route subscription operations. This is useful if you have more advanced infrastructure requirements that prevent websockets from being handled at the same path (e.g., a WSGI server listening at ``/graphql`` and an ASGI server listening at ``/ws/graphql``).
Default: ``None``
.. code:: python
GRAPHENE = {
'SUBSCRIPTION_PATH': "/ws/graphql"
}
``GRAPHIQL_HEADER_EDITOR_ENABLED``
----------------------------------
GraphiQL starting from version 1.0.0 allows setting custom headers in similar fashion to query variables.
Set to ``False`` if you want to disable GraphiQL headers editor tab for some reason.
This setting is passed to ``headerEditorEnabled`` GraphiQL options, for details refer to GraphiQLDocs_.
Default: ``True``
.. code:: python
GRAPHENE = {
'GRAPHIQL_HEADER_EDITOR_ENABLED': True,
}
``TESTING_ENDPOINT``
--------------------
Define the graphql endpoint url used for the `GraphQLTestCase` class.
Default: ``/graphql``
.. code:: python
GRAPHENE = {
'TESTING_ENDPOINT': '/customEndpoint'
}
``GRAPHIQL_SHOULD_PERSIST_HEADERS``
-----------------------------------
Set to ``True`` if you want to persist GraphiQL headers after refreshing the page.
This setting is passed to ``shouldPersistHeaders`` GraphiQL options, for details refer to GraphiQLDocs_.
Default: ``False``
.. code:: python
GRAPHENE = {
'GRAPHIQL_SHOULD_PERSIST_HEADERS': False,
}
``GRAPHIQL_INPUT_VALUE_DEPRECATION``
------------------------------------
Set to ``True`` if you want GraphiQL to show any deprecated fields on input object types' docs.
For example, having this schema:
.. code:: python
class MyMutationInputType(graphene.InputObjectType):
old_field = graphene.String(deprecation_reason="You should now use 'newField' instead.")
new_field = graphene.String()
class MyMutation(graphene.Mutation):
class Arguments:
input = types.MyMutationInputType()
GraphiQL will add a ``Show Deprecated Fields`` button to toggle information display on ``oldField`` and its deprecation
reason. Otherwise, you would get neither a button nor any information at all on ``oldField``.
This setting is passed to ``inputValueDeprecation`` GraphiQL options, for details refer to GraphiQLDocs_.
Default: ``False``
.. code:: python
GRAPHENE = {
'GRAPHIQL_INPUT_VALUE_DEPRECATION': False,
}
.. _GraphiQLDocs: https://graphiql-test.netlify.app/typedoc/modules/graphiql_react#graphiqlprovider-2
``MAX_VALIDATION_ERRORS``
------------------------------------
In case ``validation_rules`` are provided to ``GraphQLView``, if this is set to a non-negative ``int`` value,
``graphql.validation.validate`` will stop validation after this number of errors has been reached.
If not set or set to ``None``, the maximum number of errors will follow ``graphql.validation.validate`` default
*i.e.* 100.
Default: ``None``

42
docs/subscriptions.rst Normal file
View File

@ -0,0 +1,42 @@
Subscriptions
=============
The ``graphene-django`` project does not currently support GraphQL subscriptions out of the box. However, there are
several community-driven modules for adding subscription support, and the provided GraphiQL interface supports
running subscription operations over a websocket.
To implement websocket-based support for GraphQL subscriptions, youll need to do the following:
1. Install and configure `django-channels <https://channels.readthedocs.io/en/latest/installation.html>`_.
2. Install and configure* a third-party module for adding subscription support over websockets. A few options include:
- `graphql-python/graphql-ws <https://github.com/graphql-python/graphql-ws>`_
- `datavance/django-channels-graphql-ws <https://github.com/datadvance/DjangoChannelsGraphqlWs>`_
- `jaydenwindle/graphene-subscriptions <https://github.com/jaydenwindle/graphene-subscriptions>`_
3. Ensure that your application (or at least your GraphQL endpoint) is being served via an ASGI protocol server like
daphne (built in to ``django-channels``), `uvicorn <https://www.uvicorn.org/>`_, or
`hypercorn <https://pgjones.gitlab.io/hypercorn/>`_.
..
*** Note:** By default, the GraphiQL interface that comes with
``graphene-django`` assumes that you are handling subscriptions at
the same path as any other operation (i.e., you configured both
``urls.py`` and ``routing.py`` to handle GraphQL operations at the
same path, like ``/graphql``).
If these URLs differ, GraphiQL will try to run your subscription over
HTTP, which will produce an error. If you need to use a different URL
for handling websocket connections, you can configure
``SUBSCRIPTION_PATH`` in your ``settings.py``:
.. code:: python
GRAPHENE = {
# ...
"SUBSCRIPTION_PATH": "/ws/graphql" # The path you configured in `routing.py`, including a leading slash.
}
Once your application is properly configured to handle subscriptions, you can use the GraphiQL interface to test
subscriptions like any other operation.

View File

@ -1,8 +1,14 @@
Testing API calls with django
=============================
Using unittest
--------------
If you want to unittest your API calls derive your test case from the class `GraphQLTestCase`.
The default endpoint for testing is `/graphql`. You can override this in the `settings <https://docs.graphene-python.org/projects/django/en/latest/settings/#testing-endpoint>`__.
Usage:
.. code:: python
@ -10,12 +16,8 @@ Usage:
import json
from graphene_django.utils.testing 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(
'''
@ -26,7 +28,29 @@ Usage:
}
}
''',
op_name='myModel'
operation_name='myModel'
)
content = json.loads(response.content)
# This validates the status code and if you get errors
self.assertResponseNoErrors(response)
# Add some more asserts if you like
...
def test_query_with_variables(self):
response = self.query(
'''
query myModel($id: Int!){
myModel(id: $id) {
id
name
}
}
''',
operation_name='myModel',
variables={'id': 1}
)
content = json.loads(response.content)
@ -49,7 +73,7 @@ Usage:
}
}
''',
op_name='myMutation',
operation_name='myMutation',
input_data={'my_field': 'foo', 'other_field': 'bar'}
)
@ -58,3 +82,74 @@ Usage:
# Add some more asserts if you like
...
For testing mutations that are executed within a transaction you should subclass `GraphQLTransactionTestCase`
Usage:
.. code:: python
import json
from graphene_django.utils.testing import GraphQLTransactionTestCase
class MyFancyTransactionTestCase(GraphQLTransactionTestCase):
def test_some_mutation_that_executes_within_a_transaction(self):
response = self.query(
'''
mutation myMutation($input: MyMutationInput!) {
myMutation(input: $input) {
my-model {
id
name
}
}
}
''',
operation_name='myMutation',
input_data={'my_field': 'foo', 'other_field': 'bar'}
)
# This validates the status code and if you get errors
self.assertResponseNoErrors(response)
# Add some more asserts if you like
...
Using pytest
------------
To use pytest define a simple fixture using the query helper below
.. code:: python
# Create a fixture using the graphql_query helper and `client` fixture from `pytest-django`.
import json
import pytest
from graphene_django.utils.testing import graphql_query
@pytest.fixture
def client_query(client):
def func(*args, **kwargs):
return graphql_query(*args, **kwargs, client=client)
return func
# Test you query using the client_query fixture
def test_some_query(client_query):
response = client_query(
'''
query {
myModel {
id
name
}
}
''',
operation_name='myModel'
)
content = json.loads(response.content)
assert 'errors' not in content

View File

@ -3,15 +3,11 @@ Basic Tutorial
Graphene Django has a number of additional features that are designed to make
working with Django easy. Our primary focus in this tutorial is to give a good
understanding of how to connect models from Django ORM to graphene object types.
understanding of how to connect models from Django ORM to Graphene object types.
Set up the Django project
-------------------------
You can find the entire project in ``examples/cookbook-plain``.
----
We will set up the project, create the following:
- A Django project called ``cookbook``
@ -28,18 +24,18 @@ We will set up the project, create the following:
source env/bin/activate # On Windows use `env\Scripts\activate`
# Install Django and Graphene with Django support
pip install django
pip install graphene_django
pip install django graphene_django
# Set up a new project with a single application
django-admin.py startproject cookbook . # Note the trailing '.' character
django-admin startproject cookbook . # Note the trailing '.' character
cd cookbook
django-admin.py startapp ingredients
django-admin startapp ingredients
Now sync your database for the first time:
.. code:: bash
cd ..
python manage.py migrate
Let's create a few simple models...
@ -54,19 +50,18 @@ Let's get started with these models:
# cookbook/ingredients/models.py
from django.db import models
class Category(models.Model):
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class Ingredient(models.Model):
name = models.CharField(max_length=100)
notes = models.TextField()
category = models.ForeignKey(
Category, related_name='ingredients', on_delete=models.CASCADE)
Category, related_name="ingredients", on_delete=models.CASCADE
)
def __str__(self):
return self.name
@ -75,12 +70,26 @@ Add ingredients as INSTALLED_APPS:
.. code:: python
# cookbook/settings.py
INSTALLED_APPS = [
...
# Install the ingredients app
'cookbook.ingredients',
"cookbook.ingredients",
]
Make sure the app name in ``cookbook.ingredients.apps.IngredientsConfig`` is set to ``cookbook.ingredients``.
.. code:: python
# cookbook/ingredients/apps.py
from django.apps import AppConfig
class IngredientsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'cookbook.ingredients'
Don't forget to create & run migrations:
@ -95,20 +104,20 @@ Load some test data
Now is a good time to load up some test data. The easiest option will be
to `download the
ingredients.json <https://raw.githubusercontent.com/graphql-python/graphene-django/master/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json>`__
ingredients.json <https://raw.githubusercontent.com/graphql-python/graphene-django/main/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json>`__
fixture and place it in
``cookbook/ingredients/fixtures/ingredients.json``. You can then run the
following:
.. code:: bash
$ python ./manage.py loaddata ingredients
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``).
create a login for yourself too (``python manage.py createsuperuser``).
Register models with admin panel:
@ -138,66 +147,48 @@ order to create this representation, Graphene needs to know about each
This graph also has a *root type* through which all access begins. This
is the ``Query`` class below.
This means, for each of our models, we are going to create a type, subclassing ``DjangoObjectType``
To create GraphQL types for each of our Django models, we are going to subclass the ``DjangoObjectType`` class which will automatically define GraphQL fields that correspond to the fields on the Django models.
After we've done that, we will list those types as fields in the ``Query`` class.
Create ``cookbook/ingredients/schema.py`` and type the following:
Create ``cookbook/schema.py`` and type the following:
.. code:: python
# cookbook/ingredients/schema.py
# cookbook/schema.py
import graphene
from graphene_django.types import DjangoObjectType
from graphene_django import DjangoObjectType
from cookbook.ingredients.models import Category, Ingredient
class CategoryType(DjangoObjectType):
class Meta:
model = Category
fields = ("id", "name", "ingredients")
class IngredientType(DjangoObjectType):
class Meta:
model = Ingredient
fields = ("id", "name", "notes", "category")
class Query(object):
all_categories = graphene.List(CategoryType)
class Query(graphene.ObjectType):
all_ingredients = graphene.List(IngredientType)
category_by_name = graphene.Field(CategoryType, name=graphene.String(required=True))
def resolve_all_categories(self, info, **kwargs):
return Category.objects.all()
def resolve_all_ingredients(self, info, **kwargs):
def resolve_all_ingredients(root, info):
# We can easily optimize query count in the resolve method
return Ingredient.objects.select_related('category').all()
return Ingredient.objects.select_related("category").all()
Note that the above ``Query`` class is a mixin, inheriting from
``object``. This is because we will now create a project-level query
class which will combine all our app-level mixins.
Create the parent project-level ``cookbook/schema.py``:
.. code:: python
import graphene
import cookbook.ingredients.schema
class Query(cookbook.ingredients.schema.Query, graphene.ObjectType):
# This class will inherit from multiple Queries
# as we begin to add more apps to our project
pass
def resolve_category_by_name(root, info, name):
try:
return Category.objects.get(name=name)
except Category.DoesNotExist:
return None
schema = graphene.Schema(query=Query)
You can think of this as being something like your top-level ``urls.py``
file (although it currently lacks any namespacing).
file.
Testing everything so far
-------------------------
@ -216,18 +207,21 @@ Add ``graphene_django`` to ``INSTALLED_APPS`` in ``cookbook/settings.py``:
.. code:: python
# cookbook/settings.py
INSTALLED_APPS = [
...
# This will also make the `graphql_schema` management command available
'graphene_django',
"graphene_django",
]
And then add the ``SCHEMA`` to the ``GRAPHENE`` config in ``cookbook/settings.py``:
.. code:: python
# cookbook/settings.py
GRAPHENE = {
'SCHEMA': 'cookbook.schema.schema'
"SCHEMA": "cookbook.schema.schema"
}
Alternatively, we can specify the schema to be used in the urls definition,
@ -245,14 +239,17 @@ aforementioned GraphiQL we specify that on the parameters with ``graphiql=True``
.. code:: python
from django.conf.urls import url, include
# cookbook/urls.py
from django.contrib import admin
from django.urls import path
from django.views.decorators.csrf import csrf_exempt
from graphene_django.views import GraphQLView
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^graphql$', GraphQLView.as_view(graphiql=True)),
path("admin/", admin.site.urls),
path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True))),
]
@ -261,16 +258,19 @@ as explained above, we can do so here using:
.. code:: python
from django.conf.urls import url, include
# cookbook/urls.py
from django.contrib import admin
from django.urls import path
from django.views.decorators.csrf import csrf_exempt
from graphene_django.views import GraphQLView
from cookbook.schema import schema
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^graphql$', GraphQLView.as_view(graphiql=True, schema=schema)),
path("admin/", admin.site.urls),
path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True, schema=schema))),
]
@ -283,10 +283,10 @@ from the command line.
.. code:: bash
$ python ./manage.py runserver
python manage.py runserver
Performing system checks...
Django version 1.9, using settings 'cookbook.settings'
Django version 3.0.7, using settings 'cookbook.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
@ -329,24 +329,25 @@ If you are using the provided fixtures, you will see the following response:
}
}
You can experiment with ``allCategories`` too.
Something to have in mind is the `auto camelcasing <http://docs.graphene-python.org/en/latest/types/schema/#auto-camelcase-field-names>`__ that is happening.
Congratulations, you have created a working GraphQL server 🥳!
Note: Graphene `automatically camelcases <http://docs.graphene-python.org/en/latest/types/schema/#auto-camelcase-field-names>`__ all field names for better compatibility with JavaScript clients.
Getting relations
-----------------
Right now, with this simple setup in place, we can query for relations too. This is where graphql becomes really powerful!
Using the current schema we can query for relations too. This is where GraphQL becomes really powerful!
For example, we may want to list all categories and in each category, all ingredients that are in that category.
For example, we may want to get a specific categories and list all ingredients that are in that category.
We can do that with the following query:
.. code::
query {
allCategories {
categoryByName(name: "Dairy") {
id
name
ingredients {
@ -356,43 +357,26 @@ We can do that with the following query:
}
}
This will give you (in case you are using the fixtures) the following result:
.. code::
{
"data": {
"allCategories": [
{
"id": "1",
"name": "Dairy",
"ingredients": [
{
"id": "1",
"name": "Eggs"
},
{
"id": "2",
"name": "Milk"
}
]
},
{
"id": "2",
"name": "Meat",
"ingredients": [
{
"id": "3",
"name": "Beef"
},
{
"id": "4",
"name": "Chicken"
}
]
}
]
"categoryByName": {
"id": "1",
"name": "Dairy",
"ingredients": [
{
"id": "1",
"name": "Eggs"
},
{
"id": "2",
"name": "Milk"
}
]
}
}
}
@ -411,128 +395,12 @@ We can also list all ingredients and get information for the category they are i
}
}
Getting single objects
----------------------
So far, we have been able to fetch list of objects and follow relation. But what about single objects?
We can update our schema to support that, by adding new query for ``ingredient`` and ``category`` and adding arguments, so we can query for specific objects.
.. code:: python
import graphene
from graphene_django.types import DjangoObjectType
from cookbook.ingredients.models import Category, Ingredient
class CategoryType(DjangoObjectType):
class Meta:
model = Category
class IngredientType(DjangoObjectType):
class Meta:
model = Ingredient
class Query(object):
category = graphene.Field(CategoryType,
id=graphene.Int(),
name=graphene.String())
all_categories = graphene.List(CategoryType)
ingredient = graphene.Field(IngredientType,
id=graphene.Int(),
name=graphene.String())
all_ingredients = graphene.List(IngredientType)
def resolve_all_categories(self, info, **kwargs):
return Category.objects.all()
def resolve_all_ingredients(self, info, **kwargs):
return Ingredient.objects.all()
def resolve_category(self, info, **kwargs):
id = kwargs.get('id')
name = kwargs.get('name')
if id is not None:
return Category.objects.get(pk=id)
if name is not None:
return Category.objects.get(name=name)
return None
def resolve_ingredient(self, info, **kwargs):
id = kwargs.get('id')
name = kwargs.get('name')
if id is not None:
return Ingredient.objects.get(pk=id)
if name is not None:
return Ingredient.objects.get(name=name)
return None
Now, with the code in place, we can query for single objects.
For example, lets query ``category``:
.. code::
query {
category(id: 1) {
name
}
anotherCategory: category(name: "Dairy") {
ingredients {
id
name
}
}
}
This will give us the following results:
.. code::
{
"data": {
"category": {
"name": "Dairy"
},
"anotherCategory": {
"ingredients": [
{
"id": "1",
"name": "Eggs"
},
{
"id": "2",
"name": "Milk"
}
]
}
}
}
As an exercise, you can try making some queries to ``ingredient``.
Something to keep in mind - since we are using one field several times in our query, we need `aliases <http://graphql.org/learn/queries/#aliases>`__
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``.
As you can see, GraphQL is very powerful and integrating Django models allows you to get started with a working server quickly.
If you want to put things like ``django-filter`` and automatic pagination in action, you should continue with the **relay tutorial.**
If you want to put things like ``django-filter`` and automatic pagination in action, you should continue with the :ref:`Relay tutorial`.
A good idea is to check the `graphene <http://docs.graphene-python.org/en/latest/>`__
documentation but it is not essential to understand and use Graphene-Django in your project.
A good idea is to check the `Graphene <http://docs.graphene-python.org/en/latest/>`__
documentation so that you are familiar with it as well.

View File

@ -1,3 +1,5 @@
.. _Relay tutorial:
Relay tutorial
========================================
@ -5,12 +7,12 @@ Graphene has a number of additional features that are designed to make
working with Django *really simple*.
Note: The code in this quickstart is pulled from the `cookbook example
app <https://github.com/graphql-python/graphene-django/tree/master/examples/cookbook>`__.
app <https://github.com/graphql-python/graphene-django/tree/main/examples/cookbook>`__.
A good idea is to check the following things first:
* `Graphene Relay documentation <http://docs.graphene-python.org/en/latest/relay/>`__
* `GraphQL Relay Specification <https://facebook.github.io/relay/docs/en/graphql-server-specification.html>`__
* `GraphQL Relay Specification <https://relay.dev/docs/guides/graphql-server-specification/>`__
Setup the Django project
------------------------
@ -68,7 +70,7 @@ Let's get started with these models:
class Ingredient(models.Model):
name = models.CharField(max_length=100)
notes = models.TextField()
category = models.ForeignKey(Category, related_name='ingredients')
category = models.ForeignKey(Category, related_name='ingredients', on_delete=models.CASCADE)
def __str__(self):
return self.name
@ -85,7 +87,7 @@ Load some test data
Now is a good time to load up some test data. The easiest option will be
to `download the
ingredients.json <https://raw.githubusercontent.com/graphql-python/graphene-django/master/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json>`__
ingredients.json <https://raw.githubusercontent.com/graphql-python/graphene-django/main/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json>`__
fixture and place it in
``cookbook/ingredients/fixtures/ingredients.json``. You can then run the
following:
@ -130,6 +132,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following:
class CategoryNode(DjangoObjectType):
class Meta:
model = Category
fields = '__all__'
filter_fields = ['name', 'ingredients']
interfaces = (relay.Node, )
@ -137,6 +140,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following:
class IngredientNode(DjangoObjectType):
class Meta:
model = Ingredient
fields = '__all__'
# Allow for some more advanced filtering here
filter_fields = {
'name': ['exact', 'icontains', 'istartswith'],
@ -147,7 +151,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following:
interfaces = (relay.Node, )
class Query(graphene.ObjectType):
class Query(ObjectType):
category = relay.Node.Field(CategoryNode)
all_categories = DjangoFilterConnectionField(CategoryNode)
@ -277,7 +281,7 @@ from the command line.
$ python ./manage.py runserver
Performing system checks...
Django version 1.9, using settings 'cookbook.settings'
Django version 3.1.7, using settings 'cookbook.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

29
docs/validation.rst Normal file
View File

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

0
examples/__init__.py Normal file
View File

View File

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

View File

View File

@ -5,8 +5,8 @@ from cookbook.ingredients.models import Category, Ingredient
@admin.register(Ingredient)
class IngredientAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'category')
list_editable = ('name', 'category')
list_display = ("id", "name", "category")
list_editable = ("name", "category")
admin.site.register(Category)

View File

@ -2,6 +2,6 @@ from django.apps import AppConfig
class IngredientsConfig(AppConfig):
name = 'cookbook.ingredients'
label = 'ingredients'
verbose_name = 'Ingredients'
name = "cookbook.ingredients"
label = "ingredients"
verbose_name = "Ingredients"

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,8 @@ from django.db import models
class Category(models.Model):
class Meta:
verbose_name_plural = 'Categories'
verbose_name_plural = "Categories"
name = models.CharField(max_length=100)
def __str__(self):
@ -13,7 +14,9 @@ class Category(models.Model):
class Ingredient(models.Model):
name = models.CharField(max_length=100)
notes = models.TextField(null=True, blank=True)
category = models.ForeignKey(Category, related_name='ingredients', on_delete=models.CASCADE)
category = models.ForeignKey(
Category, related_name="ingredients", on_delete=models.CASCADE
)
def __str__(self):
return self.name

View File

@ -7,22 +7,22 @@ from .models import Category, Ingredient
class CategoryType(DjangoObjectType):
class Meta:
model = Category
fields = "__all__"
class IngredientType(DjangoObjectType):
class Meta:
model = Ingredient
fields = "__all__"
class Query(object):
category = graphene.Field(CategoryType,
id=graphene.Int(),
name=graphene.String())
class Query:
category = graphene.Field(CategoryType, id=graphene.Int(), name=graphene.String())
all_categories = graphene.List(CategoryType)
ingredient = graphene.Field(IngredientType,
id=graphene.Int(),
name=graphene.String())
ingredient = graphene.Field(
IngredientType, id=graphene.Int(), name=graphene.String()
)
all_ingredients = graphene.List(IngredientType)
def resolve_all_categories(self, context):
@ -30,7 +30,7 @@ class Query(object):
def resolve_all_ingredients(self, context):
# We can easily optimize query count in the resolve method
return Ingredient.objects.select_related('category').all()
return Ingredient.objects.select_related("category").all()
def resolve_category(self, context, id=None, name=None):
if id is not None:

View File

@ -1,2 +1 @@
# Create your tests here.

View File

@ -1,2 +1 @@
# Create your views here.

View File

@ -2,6 +2,6 @@ from django.apps import AppConfig
class RecipesConfig(AppConfig):
name = 'cookbook.recipes'
label = 'recipes'
verbose_name = 'Recipes'
name = "cookbook.recipes"
label = "recipes"
verbose_name = "Recipes"

View File

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

View File

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

View File

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

View File

@ -6,17 +6,23 @@ from ..ingredients.models import Ingredient
class Recipe(models.Model):
title = models.CharField(max_length=100)
instructions = models.TextField()
def __str__(self):
return self.title
class RecipeIngredient(models.Model):
recipe = models.ForeignKey(Recipe, related_name='amounts', on_delete=models.CASCADE)
ingredient = models.ForeignKey(Ingredient, related_name='used_by', on_delete=models.CASCADE)
recipe = models.ForeignKey(Recipe, related_name="amounts", on_delete=models.CASCADE)
ingredient = models.ForeignKey(
Ingredient, related_name="used_by", on_delete=models.CASCADE
)
amount = models.FloatField()
unit = models.CharField(max_length=20, choices=(
('unit', 'Units'),
('kg', 'Kilograms'),
('l', 'Litres'),
('st', 'Shots'),
))
unit = models.CharField(
max_length=20,
choices=(
("unit", "Units"),
("kg", "Kilograms"),
("l", "Litres"),
("st", "Shots"),
),
)

View File

@ -7,21 +7,20 @@ from .models import Recipe, RecipeIngredient
class RecipeType(DjangoObjectType):
class Meta:
model = Recipe
fields = "__all__"
class RecipeIngredientType(DjangoObjectType):
class Meta:
model = RecipeIngredient
fields = "__all__"
class Query(object):
recipe = graphene.Field(RecipeType,
id=graphene.Int(),
title=graphene.String())
class Query:
recipe = graphene.Field(RecipeType, id=graphene.Int(), title=graphene.String())
all_recipes = graphene.List(RecipeType)
recipeingredient = graphene.Field(RecipeIngredientType,
id=graphene.Int())
recipeingredient = graphene.Field(RecipeIngredientType, id=graphene.Int())
all_recipeingredients = graphene.List(RecipeIngredientType)
def resolve_recipe(self, context, id=None, title=None):
@ -43,5 +42,5 @@ class Query(object):
return Recipe.objects.all()
def resolve_all_recipeingredients(self, context):
related = ['recipe', 'ingredient']
related = ["recipe", "ingredient"]
return RecipeIngredient.objects.select_related(*related).all()

View File

@ -1,2 +1 @@
# Create your tests here.

View File

@ -1,2 +1 @@
# Create your views here.

View File

@ -1,14 +1,16 @@
import cookbook.ingredients.schema
import cookbook.recipes.schema
import graphene
from graphene_django.debug import DjangoDebug
import cookbook.ingredients.schema
import cookbook.recipes.schema
class Query(cookbook.ingredients.schema.Query,
cookbook.recipes.schema.Query,
graphene.ObjectType):
debug = graphene.Field(DjangoDebug, name='_debug')
class Query(
cookbook.ingredients.schema.Query,
cookbook.recipes.schema.Query,
graphene.ObjectType,
):
debug = graphene.Field(DjangoDebug, name="_debug")
schema = graphene.Schema(query=Query)

View File

@ -5,10 +5,10 @@ Django settings for cookbook project.
Generated by 'django-admin startproject' using Django 1.9.
For more information on this file, see
https://docs.djangoproject.com/en/1.9/topics/settings/
https://docs.djangoproject.com/en/3.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.9/ref/settings/
https://docs.djangoproject.com/en/3.2/ref/settings/
"""
import os
@ -18,10 +18,10 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4'
SECRET_KEY = "_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
@ -32,93 +32,86 @@ ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'graphene_django',
'cookbook.ingredients.apps.IngredientsConfig',
'cookbook.recipes.apps.RecipesConfig',
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"graphene_django",
"cookbook.ingredients.apps.IngredientsConfig",
"cookbook.recipes.apps.RecipesConfig",
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
GRAPHENE = {
'SCHEMA': 'cookbook.schema.schema',
'SCHEMA_INDENT': 2,
'MIDDLEWARE': (
'graphene_django.debug.DjangoDebugMiddleware',
)
"SCHEMA": "cookbook.schema.schema",
"SCHEMA_INDENT": 2,
"MIDDLEWARE": ("graphene_django.debug.DjangoDebugMiddleware",),
}
ROOT_URLCONF = 'cookbook.urls'
ROOT_URLCONF = "cookbook.urls"
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
]
},
},
}
]
WSGI_APPLICATION = 'cookbook.wsgi.application'
WSGI_APPLICATION = "cookbook.wsgi.application"
# Database
# https://docs.djangoproject.com/en/1.9/ref/settings/#databases
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
}
}
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# Password validation
# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
# Internationalization
# https://docs.djangoproject.com/en/1.9/topics/i18n/
# https://docs.djangoproject.com/en/3.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = "en-us"
TIME_ZONE = 'UTC'
TIME_ZONE = "UTC"
USE_I18N = True
@ -128,6 +121,6 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.9/howto/static-files/
# https://docs.djangoproject.com/en/3.2/howto/static-files/
STATIC_URL = '/static/'
STATIC_URL = "/static/"

View File

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

View File

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

View File

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

View File

View File

@ -5,8 +5,8 @@ from cookbook.ingredients.models import Category, Ingredient
@admin.register(Ingredient)
class IngredientAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'category')
list_editable = ('name', 'category')
list_display = ("id", "name", "category")
list_editable = ("name", "category")
admin.site.register(Category)

View File

@ -2,6 +2,6 @@ from django.apps import AppConfig
class IngredientsConfig(AppConfig):
name = 'cookbook.ingredients'
label = 'ingredients'
verbose_name = 'Ingredients'
name = "cookbook.ingredients"
label = "ingredients"
verbose_name = "Ingredients"

View File

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

View File

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

View File

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

View File

@ -11,7 +11,9 @@ class Category(models.Model):
class Ingredient(models.Model):
name = models.CharField(max_length=100)
notes = models.TextField(null=True, blank=True)
category = models.ForeignKey(Category, related_name='ingredients')
category = models.ForeignKey(
Category, related_name="ingredients", on_delete=models.CASCADE
)
def __str__(self):
return self.name

View File

@ -1,34 +1,35 @@
from cookbook.ingredients.models import Category, Ingredient
from graphene import Node
from graphene_django.filter import DjangoFilterConnectionField
from graphene_django.types import DjangoObjectType
from cookbook.ingredients.models import Category, Ingredient
# Graphene will automatically map the Category model's fields onto the CategoryNode.
# This is configured in the CategoryNode's Meta class (as you can see below)
class CategoryNode(DjangoObjectType):
class Meta:
model = Category
interfaces = (Node, )
filter_fields = ['name', 'ingredients']
interfaces = (Node,)
fields = "__all__"
filter_fields = ["name", "ingredients"]
class IngredientNode(DjangoObjectType):
class Meta:
model = Ingredient
# Allow for some more advanced filtering here
interfaces = (Node, )
interfaces = (Node,)
fields = "__all__"
filter_fields = {
'name': ['exact', 'icontains', 'istartswith'],
'notes': ['exact', 'icontains'],
'category': ['exact'],
'category__name': ['exact'],
"name": ["exact", "icontains", "istartswith"],
"notes": ["exact", "icontains"],
"category": ["exact"],
"category__name": ["exact"],
}
class Query(object):
class Query:
category = Node.Field(CategoryNode)
all_categories = DjangoFilterConnectionField(CategoryNode)

View File

@ -1,2 +1 @@
# Create your tests here.

View File

@ -1,2 +1 @@
# Create your views here.

View File

@ -2,6 +2,6 @@ from django.apps import AppConfig
class RecipesConfig(AppConfig):
name = 'cookbook.recipes'
label = 'recipes'
verbose_name = 'Recipes'
name = "cookbook.recipes"
label = "recipes"
verbose_name = "Recipes"

View File

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

View File

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

View File

@ -6,16 +6,23 @@ from cookbook.ingredients.models import Ingredient
class Recipe(models.Model):
title = models.CharField(max_length=100)
instructions = models.TextField()
__unicode__ = lambda self: self.title
def __unicode__(self):
return self.title
class RecipeIngredient(models.Model):
recipe = models.ForeignKey(Recipe, related_name='amounts')
ingredient = models.ForeignKey(Ingredient, related_name='used_by')
recipe = models.ForeignKey(Recipe, related_name="amounts", on_delete=models.CASCADE)
ingredient = models.ForeignKey(
Ingredient, related_name="used_by", on_delete=models.CASCADE
)
amount = models.FloatField()
unit = models.CharField(max_length=20, choices=(
('unit', 'Units'),
('kg', 'Kilograms'),
('l', 'Litres'),
('st', 'Shots'),
))
unit = models.CharField(
max_length=20,
choices=(
("unit", "Units"),
("kg", "Kilograms"),
("l", "Litres"),
("st", "Shots"),
),
)

View File

@ -1,30 +1,32 @@
from cookbook.recipes.models import Recipe, RecipeIngredient
from graphene import Node
from graphene_django.filter import DjangoFilterConnectionField
from graphene_django.types import DjangoObjectType
class RecipeNode(DjangoObjectType):
from cookbook.recipes.models import Recipe, RecipeIngredient
class RecipeNode(DjangoObjectType):
class Meta:
model = Recipe
interfaces = (Node, )
filter_fields = ['title','amounts']
interfaces = (Node,)
fields = "__all__"
filter_fields = ["title", "amounts"]
class RecipeIngredientNode(DjangoObjectType):
class Meta:
model = RecipeIngredient
# Allow for some more advanced filtering here
interfaces = (Node, )
interfaces = (Node,)
fields = "__all__"
filter_fields = {
'ingredient__name': ['exact', 'icontains', 'istartswith'],
'recipe': ['exact'],
'recipe__title': ['icontains'],
"ingredient__name": ["exact", "icontains", "istartswith"],
"recipe": ["exact"],
"recipe__title": ["icontains"],
}
class Query(object):
class Query:
recipe = Node.Field(RecipeNode)
all_recipes = DjangoFilterConnectionField(RecipeNode)

View File

@ -1,2 +1 @@
# Create your tests here.

View File

@ -1,2 +1 @@
# Create your views here.

View File

@ -1,14 +1,16 @@
import cookbook.ingredients.schema
import cookbook.recipes.schema
import graphene
from graphene_django.debug import DjangoDebug
import cookbook.ingredients.schema
import cookbook.recipes.schema
class Query(cookbook.ingredients.schema.Query,
cookbook.recipes.schema.Query,
graphene.ObjectType):
debug = graphene.Field(DjangoDebug, name='_debug')
class Query(
cookbook.ingredients.schema.Query,
cookbook.recipes.schema.Query,
graphene.ObjectType,
):
debug = graphene.Field(DjangoDebug, name="_debug")
schema = graphene.Schema(query=Query)

View File

@ -21,7 +21,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4'
SECRET_KEY = "_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
@ -32,65 +32,62 @@ ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'graphene_django',
'cookbook.ingredients.apps.IngredientsConfig',
'cookbook.recipes.apps.RecipesConfig',
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"graphene_django",
"cookbook.ingredients.apps.IngredientsConfig",
"cookbook.recipes.apps.RecipesConfig",
"django_filters",
]
MIDDLEWARE_CLASSES = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
GRAPHENE = {
'SCHEMA': 'cookbook.schema.schema',
'SCHEMA_INDENT': 2,
'MIDDLEWARE': (
'graphene_django.debug.DjangoDebugMiddleware',
)
"SCHEMA": "cookbook.schema.schema",
"SCHEMA_INDENT": 2,
"MIDDLEWARE": ("graphene_django.debug.DjangoDebugMiddleware",),
}
ROOT_URLCONF = 'cookbook.urls'
ROOT_URLCONF = "cookbook.urls"
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
]
},
},
}
]
WSGI_APPLICATION = 'cookbook.wsgi.application'
WSGI_APPLICATION = "cookbook.wsgi.application"
# Database
# https://docs.djangoproject.com/en/1.9/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
}
}
@ -100,26 +97,20 @@ DATABASES = {
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
# Internationalization
# https://docs.djangoproject.com/en/1.9/topics/i18n/
LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = "en-us"
TIME_ZONE = 'UTC'
TIME_ZONE = "UTC"
USE_I18N = True
@ -131,4 +122,4 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.9/howto/static-files/
STATIC_URL = '/static/'
STATIC_URL = "/static/"

View File

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

View File

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

View File

@ -1,5 +1,5 @@
graphene
graphene-django
graphql-core>=2.1rc1
django==1.11.19
graphene>=2.1,<3
graphene-django>=2.1,<3
graphql-core>=2.1,<3
django==3.2.25
django-filter>=2

View File

@ -0,0 +1,32 @@
import os
import sys
ROOT_PATH = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, ROOT_PATH + "/examples/")
SECRET_KEY = 1
INSTALLED_APPS = [
"graphene_django",
"graphene_django.rest_framework",
"graphene_django.tests",
"examples.starwars",
]
DATABASES = {
"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": "django_test.sqlite"}
}
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
}
]
GRAPHENE = {"SCHEMA": "graphene_django.tests.schema_view.schema"}
ROOT_URLCONF = "graphene_django.tests.urls"
USE_TZ = True

View File

@ -2,97 +2,50 @@ from .models import Character, Faction, Ship
def initialize():
human = Character(
name='Human'
)
human = Character(name="Human")
human.save()
droid = Character(
name='Droid'
)
droid = Character(name="Droid")
droid.save()
rebels = Faction(
id='1',
name='Alliance to Restore the Republic',
hero=human
)
rebels = Faction(id="1", name="Alliance to Restore the Republic", hero=human)
rebels.save()
empire = Faction(
id='2',
name='Galactic Empire',
hero=droid
)
empire = Faction(id="2", name="Galactic Empire", hero=droid)
empire.save()
xwing = Ship(
id='1',
name='X-Wing',
faction=rebels,
)
xwing = Ship(id="1", name="X-Wing", faction=rebels)
xwing.save()
human.ship = xwing
human.save()
ywing = Ship(
id='2',
name='Y-Wing',
faction=rebels,
)
ywing = Ship(id="2", name="Y-Wing", faction=rebels)
ywing.save()
awing = Ship(
id='3',
name='A-Wing',
faction=rebels,
)
awing = Ship(id="3", name="A-Wing", faction=rebels)
awing.save()
# Yeah, technically it's Corellian. But it flew in the service of the rebels,
# so for the purposes of this demo it's a rebel ship.
falcon = Ship(
id='4',
name='Millenium Falcon',
faction=rebels,
)
falcon = Ship(id="4", name="Millennium Falcon", faction=rebels)
falcon.save()
homeOne = Ship(
id='5',
name='Home One',
faction=rebels,
)
homeOne = Ship(id="5", name="Home One", faction=rebels)
homeOne.save()
tieFighter = Ship(
id='6',
name='TIE Fighter',
faction=empire,
)
tieFighter = Ship(id="6", name="TIE Fighter", faction=empire)
tieFighter.save()
tieInterceptor = Ship(
id='7',
name='TIE Interceptor',
faction=empire,
)
tieInterceptor = Ship(id="7", name="TIE Interceptor", faction=empire)
tieInterceptor.save()
executor = Ship(
id='8',
name='Executor',
faction=empire,
)
executor = Ship(id="8", name="Executor", faction=empire)
executor.save()
def create_ship(ship_name, faction_id):
new_ship = Ship(
name=ship_name,
faction_id=faction_id
)
new_ship = Ship(name=ship_name, faction_id=faction_id)
new_ship.save()
return new_ship

View File

@ -1,11 +1,15 @@
from __future__ import absolute_import
from django.db import models
class Character(models.Model):
name = models.CharField(max_length=50)
ship = models.ForeignKey('Ship', on_delete=models.CASCADE, blank=True, null=True, related_name='characters')
ship = models.ForeignKey(
"Ship",
on_delete=models.CASCADE,
blank=True,
null=True,
related_name="characters",
)
def __str__(self):
return self.name
@ -21,7 +25,7 @@ class Faction(models.Model):
class Ship(models.Model):
name = models.CharField(max_length=50)
faction = models.ForeignKey(Faction, on_delete=models.CASCADE, related_name='ships')
faction = models.ForeignKey(Faction, on_delete=models.CASCADE, related_name="ships")
def __str__(self):
return self.name

View File

@ -1,19 +1,20 @@
import graphene
from graphene import Schema, relay, resolve_only_args
from graphene import Schema, relay
from graphene_django import DjangoConnectionField, DjangoObjectType
from .data import (create_ship, get_empire, get_faction, get_rebels, get_ship,
get_ships)
from .models import Character as CharacterModel
from .models import Faction as FactionModel
from .models import Ship as ShipModel
from .data import create_ship, get_empire, get_faction, get_rebels, get_ship, get_ships
from .models import (
Character as CharacterModel,
Faction as FactionModel,
Ship as ShipModel,
)
class Ship(DjangoObjectType):
class Meta:
model = ShipModel
interfaces = (relay.Node, )
interfaces = (relay.Node,)
fields = "__all__"
@classmethod
def get_node(cls, info, id):
@ -22,16 +23,16 @@ class Ship(DjangoObjectType):
class Character(DjangoObjectType):
class Meta:
model = CharacterModel
fields = "__all__"
class Faction(DjangoObjectType):
class Meta:
model = FactionModel
interfaces = (relay.Node, )
interfaces = (relay.Node,)
fields = "__all__"
@classmethod
def get_node(cls, info, id):
@ -39,7 +40,6 @@ class Faction(DjangoObjectType):
class IntroduceShip(relay.ClientIDMutation):
class Input:
ship_name = graphene.String(required=True)
faction_id = graphene.String(required=True)
@ -48,7 +48,9 @@ class IntroduceShip(relay.ClientIDMutation):
faction = graphene.Field(Faction)
@classmethod
def mutate_and_get_payload(cls, root, info, ship_name, faction_id, client_mutation_id=None):
def mutate_and_get_payload(
cls, root, info, ship_name, faction_id, client_mutation_id=None
):
ship = create_ship(ship_name, faction_id)
faction = get_faction(faction_id)
return IntroduceShip(ship=ship, faction=faction)
@ -58,18 +60,15 @@ class Query(graphene.ObjectType):
rebels = graphene.Field(Faction)
empire = graphene.Field(Faction)
node = relay.Node.Field()
ships = DjangoConnectionField(Ship, description='All the ships.')
ships = DjangoConnectionField(Ship, description="All the ships.")
@resolve_only_args
def resolve_ships(self):
def resolve_ships(self, info):
return get_ships()
@resolve_only_args
def resolve_rebels(self):
def resolve_rebels(self, info):
return get_rebels()
@resolve_only_args
def resolve_empire(self):
def resolve_empire(self, info):
return get_empire()

View File

@ -8,7 +8,7 @@ pytestmark = pytest.mark.django_db
def test_correct_fetch_first_ship_rebels():
initialize()
query = '''
query = """
query RebelsShipsQuery {
rebels {
name,
@ -24,22 +24,12 @@ def test_correct_fetch_first_ship_rebels():
}
}
}
'''
"""
expected = {
'rebels': {
'name': 'Alliance to Restore the Republic',
'hero': {
'name': 'Human'
},
'ships': {
'edges': [
{
'node': {
'name': 'X-Wing'
}
}
]
}
"rebels": {
"name": "Alliance to Restore the Republic",
"hero": {"name": "Human"},
"ships": {"edges": [{"node": {"name": "X-Wing"}}]},
}
}
result = schema.execute(query)
@ -49,7 +39,7 @@ def test_correct_fetch_first_ship_rebels():
def test_correct_list_characters():
initialize()
query = '''
query = """
query RebelsShipsQuery {
node(id: "U2hpcDox") {
... on Ship {
@ -60,15 +50,8 @@ def test_correct_list_characters():
}
}
}
'''
expected = {
'node': {
'name': 'X-Wing',
'characters': [{
'name': 'Human'
}],
}
}
"""
expected = {"node": {"name": "X-Wing", "characters": [{"name": "Human"}]}}
result = schema.execute(query)
assert not result.errors
assert result.data == expected

View File

@ -9,7 +9,7 @@ pytestmark = pytest.mark.django_db
def test_mutations():
initialize()
query = '''
query = """
mutation MyMutation {
introduceShip(input:{clientMutationId:"abc", shipName: "Peter", factionId: "1"}) {
ship {
@ -29,49 +29,23 @@ def test_mutations():
}
}
}
'''
"""
expected = {
'introduceShip': {
'ship': {
'id': 'U2hpcDo5',
'name': 'Peter'
},
'faction': {
'name': 'Alliance to Restore the Republic',
'ships': {
'edges': [{
'node': {
'id': 'U2hpcDox',
'name': 'X-Wing'
}
}, {
'node': {
'id': 'U2hpcDoy',
'name': 'Y-Wing'
}
}, {
'node': {
'id': 'U2hpcDoz',
'name': 'A-Wing'
}
}, {
'node': {
'id': 'U2hpcDo0',
'name': 'Millenium Falcon'
}
}, {
'node': {
'id': 'U2hpcDo1',
'name': 'Home One'
}
}, {
'node': {
'id': 'U2hpcDo5',
'name': 'Peter'
}
}]
"introduceShip": {
"ship": {"id": "U2hpcDo5", "name": "Peter"},
"faction": {
"name": "Alliance to Restore the Republic",
"ships": {
"edges": [
{"node": {"id": "U2hpcDox", "name": "X-Wing"}},
{"node": {"id": "U2hpcDoy", "name": "Y-Wing"}},
{"node": {"id": "U2hpcDoz", "name": "A-Wing"}},
{"node": {"id": "U2hpcDo0", "name": "Millennium Falcon"}},
{"node": {"id": "U2hpcDo1", "name": "Home One"}},
{"node": {"id": "U2hpcDo5", "name": "Peter"}},
]
},
}
},
}
}
result = schema.execute(query)

View File

@ -8,19 +8,16 @@ pytestmark = pytest.mark.django_db
def test_correctly_fetches_id_name_rebels():
initialize()
query = '''
query = """
query RebelsQuery {
rebels {
id
name
}
}
'''
"""
expected = {
'rebels': {
'id': 'RmFjdGlvbjox',
'name': 'Alliance to Restore the Republic'
}
"rebels": {"id": "RmFjdGlvbjox", "name": "Alliance to Restore the Republic"}
}
result = schema.execute(query)
assert not result.errors
@ -29,7 +26,7 @@ def test_correctly_fetches_id_name_rebels():
def test_correctly_refetches_rebels():
initialize()
query = '''
query = """
query RebelsRefetchQuery {
node(id: "RmFjdGlvbjox") {
id
@ -38,12 +35,9 @@ def test_correctly_refetches_rebels():
}
}
}
'''
"""
expected = {
'node': {
'id': 'RmFjdGlvbjox',
'name': 'Alliance to Restore the Republic'
}
"node": {"id": "RmFjdGlvbjox", "name": "Alliance to Restore the Republic"}
}
result = schema.execute(query)
assert not result.errors
@ -52,20 +46,15 @@ def test_correctly_refetches_rebels():
def test_correctly_fetches_id_name_empire():
initialize()
query = '''
query = """
query EmpireQuery {
empire {
id
name
}
}
'''
expected = {
'empire': {
'id': 'RmFjdGlvbjoy',
'name': 'Galactic Empire'
}
}
"""
expected = {"empire": {"id": "RmFjdGlvbjoy", "name": "Galactic Empire"}}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
@ -73,7 +62,7 @@ def test_correctly_fetches_id_name_empire():
def test_correctly_refetches_empire():
initialize()
query = '''
query = """
query EmpireRefetchQuery {
node(id: "RmFjdGlvbjoy") {
id
@ -82,13 +71,8 @@ def test_correctly_refetches_empire():
}
}
}
'''
expected = {
'node': {
'id': 'RmFjdGlvbjoy',
'name': 'Galactic Empire'
}
}
"""
expected = {"node": {"id": "RmFjdGlvbjoy", "name": "Galactic Empire"}}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
@ -96,7 +80,7 @@ def test_correctly_refetches_empire():
def test_correctly_refetches_xwing():
initialize()
query = '''
query = """
query XWingRefetchQuery {
node(id: "U2hpcDox") {
id
@ -105,13 +89,8 @@ def test_correctly_refetches_xwing():
}
}
}
'''
expected = {
'node': {
'id': 'U2hpcDox',
'name': 'X-Wing'
}
}
"""
expected = {"node": {"id": "U2hpcDox", "name": "X-Wing"}}
result = schema.execute(query)
assert not result.errors
assert result.data == expected

View File

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

View File

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

View File

@ -0,0 +1,18 @@
import pytest
from graphene_django.settings import graphene_settings as gsettings
from .registry import reset_global_registry
@pytest.fixture(autouse=True)
def reset_registry_fixture(db):
yield None
reset_global_registry()
@pytest.fixture()
def graphene_settings():
settings = dict(gsettings.__dict__)
yield gsettings
gsettings.__dict__ = settings

View File

@ -0,0 +1 @@
MUTATION_ERRORS_FLAG = "graphene_mutation_has_errors"

View File

@ -1,9 +1,19 @@
import inspect
from functools import partial, singledispatch, wraps
from django.db import models
from django.utils.encoding import force_text
from django.utils.encoding import force_str
from django.utils.functional import Promise
from django.utils.module_loading import import_string
from graphql import GraphQLError
from graphene import (
ID,
UUID,
Boolean,
Date,
DateTime,
Decimal,
Dynamic,
Enum,
Field,
@ -12,66 +22,151 @@ from graphene import (
List,
NonNull,
String,
UUID,
DateTime,
Date,
Time,
)
from graphene.types.json import JSONString
from graphene.utils.str_converters import to_camel_case, to_const
from graphql import assert_valid_name
from graphene.types.resolver import get_default_resolver
from graphene.types.scalars import BigInt
from graphene.utils.str_converters import to_camel_case
from .compat import ArrayField, HStoreField, JSONField, RangeField
from .fields import DjangoListField, DjangoConnectionField
from .utils import import_single_dispatch
try:
from graphql import assert_name
except ImportError:
# Support for older versions of graphql
from graphql import assert_valid_name as assert_name
from graphql.pyutils import register_description
singledispatch = import_single_dispatch()
from .compat import ArrayField, HStoreField, RangeField, normalize_choices
from .fields import DjangoConnectionField, DjangoListField
from .settings import graphene_settings
from .utils.str_converters import to_const
class BlankValueField(Field):
def wrap_resolve(self, parent_resolver):
resolver = self.resolver or parent_resolver
# create custom resolver
def blank_field_wrapper(func):
@wraps(func)
def wrapped_resolver(*args, **kwargs):
return_value = func(*args, **kwargs)
if return_value == "":
return None
return return_value
return wrapped_resolver
return blank_field_wrapper(resolver)
class EnumValueField(BlankValueField):
def wrap_resolve(self, parent_resolver):
resolver = super().wrap_resolve(parent_resolver)
# create custom resolver
def enum_field_wrapper(func):
@wraps(func)
def wrapped_resolver(*args, **kwargs):
return_value = func(*args, **kwargs)
if isinstance(return_value, models.Choices):
return_value = return_value.value
return return_value
return wrapped_resolver
return enum_field_wrapper(resolver)
def convert_choice_name(name):
name = to_const(force_text(name))
name = to_const(force_str(name))
try:
assert_valid_name(name)
except AssertionError:
assert_name(name)
except GraphQLError:
name = "A_%s" % name
return name
def get_choices(choices):
converted_names = []
choices = normalize_choices(choices)
for value, help_text in choices:
if isinstance(help_text, (tuple, list)):
for choice in get_choices(help_text):
yield choice
yield from get_choices(help_text)
else:
name = convert_choice_name(value)
while name in converted_names:
name += "_" + str(len(converted_names))
converted_names.append(name)
description = help_text
description = str(
help_text
) # TODO: translatable description: https://github.com/graphql-python/graphql-core-next/issues/58
yield name, value, description
def convert_django_field_with_choices(field, registry=None):
def convert_choices_to_named_enum_with_descriptions(name, choices):
choices = list(get_choices(choices))
named_choices = [(c[0], c[1]) for c in choices]
named_choices_descriptions = {c[0]: c[2] for c in choices}
class EnumWithDescriptionsType:
@property
def description(self):
return str(named_choices_descriptions[self.name])
return_type = Enum(
name,
list(named_choices),
type=EnumWithDescriptionsType,
description="An enumeration.", # Temporary fix until https://github.com/graphql-python/graphene/pull/1502 is merged
)
return return_type
def generate_enum_name(django_model_meta, field):
if graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME:
# Try and import custom function
custom_func = import_string(
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME
)
name = custom_func(field)
elif graphene_settings.DJANGO_CHOICE_FIELD_ENUM_V2_NAMING is True:
name = to_camel_case(f"{django_model_meta.object_name}_{field.name}")
else:
name = "{app_label}{object_name}{field_name}Choices".format(
app_label=to_camel_case(django_model_meta.app_label.title()),
object_name=django_model_meta.object_name,
field_name=to_camel_case(field.name.title()),
)
return name
def convert_choice_field_to_enum(field, name=None):
if name is None:
name = generate_enum_name(field.model._meta, field)
choices = field.choices
return convert_choices_to_named_enum_with_descriptions(name, choices)
def convert_django_field_with_choices(
field, registry=None, convert_choices_to_enum=None
):
if registry is not None:
converted = registry.get_converted_field(field)
if converted:
return converted
choices = getattr(field, "choices", None)
if choices:
meta = field.model._meta
name = to_camel_case("{}_{}".format(meta.object_name, field.name))
choices = list(get_choices(choices))
named_choices = [(c[0], c[1]) for c in choices]
named_choices_descriptions = {c[0]: c[2] for c in choices}
if convert_choices_to_enum is None:
convert_choices_to_enum = bool(
graphene_settings.DJANGO_CHOICE_FIELD_ENUM_CONVERT
)
if choices and convert_choices_to_enum:
EnumCls = convert_choice_field_to_enum(field)
required = not (field.blank or field.null)
class EnumWithDescriptionsType(object):
@property
def description(self):
return named_choices_descriptions[self.name]
enum = Enum(name, list(named_choices), type=EnumWithDescriptionsType)
converted = enum(description=field.help_text, required=not field.null)
converted = EnumCls(
description=get_django_field_description(field), required=required
).mount_as(EnumValueField)
else:
converted = convert_django_field(field, registry)
if registry is not None:
@ -79,10 +174,14 @@ def convert_django_field_with_choices(field, registry=None):
return converted
def get_django_field_description(field):
return str(field.help_text) if field.help_text else None
@singledispatch
def convert_django_field(field, registry=None):
raise Exception(
"Don't know how to convert the Django field %s (%s)" % (field, field.__class__)
f"Don't know how to convert the Django field {field} ({field.__class__})"
)
@ -95,62 +194,88 @@ def convert_django_field(field, registry=None):
@convert_django_field.register(models.FileField)
@convert_django_field.register(models.FilePathField)
def convert_field_to_string(field, registry=None):
return String(description=field.help_text, required=not field.null)
return String(
description=get_django_field_description(field), required=not field.null
)
@convert_django_field.register(models.AutoField)
@convert_django_field.register(models.BigAutoField)
@convert_django_field.register(models.SmallAutoField)
def convert_field_to_id(field, registry=None):
return ID(description=field.help_text, required=not field.null)
return ID(description=get_django_field_description(field), required=not field.null)
@convert_django_field.register(models.UUIDField)
def convert_field_to_uuid(field, registry=None):
return UUID(description=field.help_text, required=not field.null)
return UUID(
description=get_django_field_description(field), required=not field.null
)
@convert_django_field.register(models.BigIntegerField)
def convert_big_int_field(field, registry=None):
return BigInt(description=field.help_text, required=not field.null)
@convert_django_field.register(models.PositiveIntegerField)
@convert_django_field.register(models.PositiveSmallIntegerField)
@convert_django_field.register(models.SmallIntegerField)
@convert_django_field.register(models.BigIntegerField)
@convert_django_field.register(models.IntegerField)
def convert_field_to_int(field, registry=None):
return Int(description=field.help_text, required=not field.null)
@convert_django_field.register(models.BooleanField)
def convert_field_to_boolean(field, registry=None):
return NonNull(Boolean, description=field.help_text)
return Int(description=get_django_field_description(field), required=not field.null)
@convert_django_field.register(models.NullBooleanField)
def convert_field_to_nullboolean(field, registry=None):
return Boolean(description=field.help_text, required=not field.null)
@convert_django_field.register(models.BooleanField)
def convert_field_to_boolean(field, registry=None):
return Boolean(
description=get_django_field_description(field), required=not field.null
)
@convert_django_field.register(models.DecimalField)
def convert_field_to_decimal(field, registry=None):
return Decimal(
description=get_django_field_description(field), required=not field.null
)
@convert_django_field.register(models.FloatField)
@convert_django_field.register(models.DurationField)
def convert_field_to_float(field, registry=None):
return Float(description=field.help_text, required=not field.null)
return Float(
description=get_django_field_description(field), required=not field.null
)
@convert_django_field.register(models.DateTimeField)
def convert_datetime_to_string(field, registry=None):
return DateTime(description=field.help_text, required=not field.null)
return DateTime(
description=get_django_field_description(field), required=not field.null
)
@convert_django_field.register(models.DateField)
def convert_date_to_string(field, registry=None):
return Date(description=field.help_text, required=not field.null)
return Date(
description=get_django_field_description(field), required=not field.null
)
@convert_django_field.register(models.TimeField)
def convert_time_to_string(field, registry=None):
return Time(description=field.help_text, required=not field.null)
return Time(
description=get_django_field_description(field), required=not field.null
)
@convert_django_field.register(models.OneToOneRel)
def convert_onetoone_field_to_djangomodel(field, registry=None):
from graphene.utils.str_converters import to_snake_case
from .types import DjangoObjectType
model = field.related_model
def dynamic_type():
@ -158,10 +283,55 @@ def convert_onetoone_field_to_djangomodel(field, registry=None):
if not _type:
return
# We do this for a bug in Django 1.8, where null attr
# is not available in the OneToOneRel instance
null = getattr(field, "null", True)
return Field(_type, required=not null)
class CustomField(Field):
def wrap_resolve(self, parent_resolver):
"""
Implements a custom resolver which goes through the `get_node` method to ensure that
it goes through the `get_queryset` method of the DjangoObjectType.
"""
resolver = super().wrap_resolve(parent_resolver)
# If `get_queryset` was not overridden in the DjangoObjectType
# or if we explicitly bypass the `get_queryset` method,
# we can just return the default resolver.
if (
_type.get_queryset.__func__
is DjangoObjectType.get_queryset.__func__
or getattr(resolver, "_bypass_get_queryset", False)
):
return resolver
def custom_resolver(root, info, **args):
# Note: this function is used to resolve 1:1 relation fields
is_resolver_awaitable = inspect.iscoroutinefunction(resolver)
if is_resolver_awaitable:
fk_obj = resolver(root, info, **args)
# In case the resolver is a custom awaitable resolver that overwrites
# the default Django resolver
return fk_obj
field_name = to_snake_case(info.field_name)
reversed_field_name = root.__class__._meta.get_field(
field_name
).remote_field.name
try:
return _type.get_queryset(
_type._meta.model.objects.filter(
**{reversed_field_name: root.pk}
),
info,
).get()
except _type._meta.model.DoesNotExist:
return None
return custom_resolver
return CustomField(
_type,
required=not field.null,
)
return Dynamic(dynamic_type)
@ -177,19 +347,31 @@ def convert_field_to_list_or_connection(field, registry=None):
if not _type:
return
if isinstance(field, models.ManyToManyField):
description = get_django_field_description(field)
else:
description = get_django_field_description(field.field)
# If there is a connection, we should transform the field
# into a DjangoConnectionField
if _type._meta.connection:
# Use a DjangoFilterConnectionField if there are
# defined filter_fields in the DjangoObjectType Meta
if _type._meta.filter_fields:
# defined filter_fields or a filterset_class in the
# DjangoObjectType Meta
if _type._meta.filter_fields or _type._meta.filterset_class:
from .filter.fields import DjangoFilterConnectionField
return DjangoFilterConnectionField(_type)
return DjangoFilterConnectionField(
_type, required=True, description=description
)
return DjangoConnectionField(_type)
return DjangoConnectionField(_type, required=True, description=description)
return DjangoListField(_type)
return DjangoListField(
_type,
required=True, # A Set is always returned, never None.
description=description,
)
return Dynamic(dynamic_type)
@ -197,6 +379,10 @@ def convert_field_to_list_or_connection(field, registry=None):
@convert_django_field.register(models.OneToOneField)
@convert_django_field.register(models.ForeignKey)
def convert_field_to_djangomodel(field, registry=None):
from graphene.utils.str_converters import to_snake_case
from .types import DjangoObjectType
model = field.related_model
def dynamic_type():
@ -204,28 +390,127 @@ def convert_field_to_djangomodel(field, registry=None):
if not _type:
return
return Field(_type, description=field.help_text, required=not field.null)
class CustomField(Field):
def wrap_resolve(self, parent_resolver):
"""
Implements a custom resolver which goes through the `get_node` method to ensure that
it goes through the `get_queryset` method of the DjangoObjectType.
"""
resolver = super().wrap_resolve(parent_resolver)
# If `get_queryset` was not overridden in the DjangoObjectType
# or if we explicitly bypass the `get_queryset` method,
# we can just return the default resolver.
if (
_type.get_queryset.__func__
is DjangoObjectType.get_queryset.__func__
or getattr(resolver, "_bypass_get_queryset", False)
):
return resolver
def custom_resolver(root, info, **args):
# Note: this function is used to resolve FK or 1:1 fields
# it does not differentiate between custom-resolved fields
# and default resolved fields.
# because this is a django foreign key or one-to-one field, the primary-key for
# this node can be accessed from the root node.
# ex: article.reporter_id
# get the name of the id field from the root's model
field_name = to_snake_case(info.field_name)
db_field_key = root.__class__._meta.get_field(field_name).attname
if hasattr(root, db_field_key):
# get the object's primary-key from root
object_pk = getattr(root, db_field_key)
else:
return None
is_resolver_awaitable = inspect.iscoroutinefunction(resolver)
if is_resolver_awaitable:
fk_obj = resolver(root, info, **args)
# In case the resolver is a custom awaitable resolver that overwrites
# the default Django resolver
return fk_obj
instance_from_get_node = _type.get_node(info, object_pk)
if instance_from_get_node is None:
# no instance to return
return
elif (
isinstance(resolver, partial)
and resolver.func is get_default_resolver()
):
return instance_from_get_node
elif resolver is not get_default_resolver():
# Default resolver is overridden
# For optimization, add the instance to the resolver
setattr(root, field_name, instance_from_get_node)
# Explanation:
# previously, _type.get_node` is called which results in at least one hit to the database.
# But, if we did not pass the instance to the root, calling the resolver will result in
# another call to get the instance which results in at least two database queries in total
# to resolve this node only.
# That's why the value of the object is set in the root so when the object is accessed
# in the resolver (root.field_name) it does not access the database unless queried explicitly.
fk_obj = resolver(root, info, **args)
return fk_obj
else:
return instance_from_get_node
return custom_resolver
return CustomField(
_type,
description=get_django_field_description(field),
required=not field.null,
)
return Dynamic(dynamic_type)
@convert_django_field.register(ArrayField)
def convert_postgres_array_to_list(field, registry=None):
base_type = convert_django_field(field.base_field)
if not isinstance(base_type, (List, NonNull)):
base_type = type(base_type)
return List(base_type, description=field.help_text, required=not field.null)
inner_type = convert_django_field(field.base_field)
if not isinstance(inner_type, (List, NonNull)):
inner_type = (
NonNull(type(inner_type))
if inner_type.kwargs["required"]
else type(inner_type)
)
return List(
inner_type,
description=get_django_field_description(field),
required=not field.null,
)
@convert_django_field.register(HStoreField)
@convert_django_field.register(JSONField)
def convert_posgres_field_to_string(field, registry=None):
return JSONString(description=field.help_text, required=not field.null)
@convert_django_field.register(models.JSONField)
def convert_json_field_to_string(field, registry=None):
return JSONString(
description=get_django_field_description(field), required=not field.null
)
@convert_django_field.register(RangeField)
def convert_posgres_range_to_string(field, registry=None):
def convert_postgres_range_to_string(field, registry=None):
inner_type = convert_django_field(field.base_field)
if not isinstance(inner_type, (List, NonNull)):
inner_type = type(inner_type)
return List(inner_type, description=field.help_text, required=not field.null)
inner_type = (
NonNull(type(inner_type))
if inner_type.kwargs["required"]
else type(inner_type)
)
return List(
inner_type,
description=get_django_field_description(field),
required=not field.null,
)
# Register Django lazy()-wrapped values as GraphQL description/help_text.
# This is needed for using lazy translations, see https://github.com/graphql-python/graphql-core-next/issues/58.
register_description(Promise)

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