mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-04-20 17:12:05 +03:00
Compare commits
670 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
c52cf2b045 | ||
|
e69e4a0399 | ||
|
97deb761e9 | ||
|
8d4a64a40d | ||
|
269225085d | ||
|
28c71c58f7 | ||
|
6f21dc7a94 | ||
|
ea45de02ad | ||
|
eac113e136 | ||
|
d69c90550f | ||
|
3f813d4679 | ||
|
45c2aa09b5 | ||
|
ac09cd2967 | ||
|
54372b41d5 | ||
|
96c09ac439 | ||
|
b85177cebf | ||
|
4d0484f312 | ||
|
c416a2b0f5 | ||
|
feb7252b8a | ||
|
3a64994e52 | ||
|
db2d40ec94 | ||
|
62126dd467 | ||
|
e735f5dbdb | ||
|
36cf100e8b | ||
|
e8f36b018d | ||
|
83d3d27f14 | ||
|
ee7560f629 | ||
|
67def2e074 | ||
|
e49a01c189 | ||
|
0473f1a9a3 | ||
|
720db1f987 | ||
|
4ac3f3f42d | ||
|
ee7598e71a | ||
|
05d7fb5396 | ||
|
79b4a23ae0 | ||
|
db34d2e815 | ||
|
9a773b9d7b | ||
|
45a732f1db | ||
|
5eb5fe294a | ||
|
5d7a04fce9 | ||
|
3172710d12 | ||
|
b1abebdb97 | ||
|
0de35ca3b0 | ||
|
2fafa881a8 | ||
|
cd43022283 | ||
|
3f061a0c50 | ||
|
e950164c8e | ||
|
2358bd30a4 | ||
|
3e7a16af73 | ||
|
8fa8aea3c0 | ||
|
c925a32dc3 | ||
|
8934393909 | ||
|
520ddeabf6 | ||
|
38709d8396 | ||
|
63fd98393f | ||
|
4e5acd4702 | ||
|
ebf49431e9 | ||
|
b75904d4c8 | ||
|
7fe661d423 | ||
|
be17278b49 | ||
|
a6596273cf | ||
|
388ca41d64 | ||
|
72a3700856 | ||
|
09f9b6d2f1 | ||
|
6f13d28b6e | ||
|
ce7492b5ae | ||
|
52f992183f | ||
|
8540a9332c | ||
|
af8888f58e | ||
|
c1a22bfd91 | ||
|
95a0642818 | ||
|
a8ceca77ed | ||
|
20a6cecc4c | ||
|
9796e93fc7 | ||
|
f67c5dbc8c | ||
|
34cc86063b | ||
|
a335042dbe | ||
|
df3c0bf75b | ||
|
7e1a1d1fb8 | ||
|
3283d0b1be | ||
|
1d814c54c4 | ||
|
0beb3385df | ||
|
3b41aaf7bf | ||
|
d18cab8aa4 | ||
|
daa0ab046b | ||
|
a000d58514 | ||
|
86c5309c45 | ||
|
4517e32224 | ||
|
f24cbd5148 | ||
|
ed7c995d8c | ||
|
bb03306075 | ||
|
c697e5c8c1 | ||
|
a78114ada3 | ||
|
07940aa5f5 | ||
|
9a60589732 | ||
|
0b2cc4ecb2 | ||
|
97442f9cee | ||
|
60b3032014 | ||
|
05d3df92e7 | ||
|
541caa117e | ||
|
0f40da7b31 | ||
|
5d81ba04f9 | ||
|
b2f83eb277 | ||
|
56892d7f4b | ||
|
4f315c365d | ||
|
a53ded611b | ||
|
37848fa2df | ||
|
3473fe025e | ||
|
42a40b4df0 | ||
|
8ae576394e | ||
|
2aeb86ba3b | ||
|
5f1731dca3 | ||
|
f6ec0689c1 | ||
|
0bb9f1ca60 | ||
|
bf8fd7696b | ||
|
775644b536 | ||
|
e1a7d19833 | ||
|
5d5d7f1815 | ||
|
32667b5407 | ||
|
ef9d67302e | ||
|
1e4b03b975 | ||
|
e7f7d8da07 | ||
|
623d0f219e | ||
|
608af578d4 | ||
|
26a851a523 | ||
|
762eaabd04 | ||
|
80ea51fc3b | ||
|
3cf940d0c8 | ||
|
3058118e8f | ||
|
594ca6e25e | ||
|
573d38e13e | ||
|
212524fd8c | ||
|
fe66b48d38 | ||
|
e9f25ecf2d | ||
|
6046a710c8 | ||
|
ea593b673f | ||
|
5cee41407c | ||
|
2d4ca0ac7b | ||
|
5ce4553244 | ||
|
007768b454 | ||
|
beb2e4aae3 | ||
|
52880166bd | ||
|
4573d3db53 | ||
|
c3404a9793 | ||
|
d9ab8acf26 | ||
|
5dea6ffa41 | ||
|
bcc7f85dad | ||
|
10e48c27b7 | ||
|
ea84827ab8 | ||
|
fdeadf5ce5 | ||
|
8324d47999 | ||
|
40e5252936 | ||
|
8c48516093 | ||
|
c049ab7470 | ||
|
2d0b9ddd42 | ||
|
e559a42374 | ||
|
dab6080fcf | ||
|
558288afce | ||
|
8f63199a63 | ||
|
a51c2bffd9 | ||
|
0e12343853 | ||
|
cc3bd05472 | ||
|
99512c53a1 | ||
|
7b35695067 | ||
|
454b74052e | ||
|
4c0c821b74 | ||
|
4b7119d691 | ||
|
eb7a0265d8 | ||
|
0888c748fd | ||
|
f554911397 | ||
|
8571bc465a | ||
|
2140be5e6a | ||
|
8408c51bf9 | ||
|
8928ec2dbe | ||
|
65f41c1a17 | ||
|
ee3d4f521f | ||
|
2e806384f6 | ||
|
86a66db1f6 | ||
|
19e3eddddb | ||
|
f5d94fda1f | ||
|
88eefb0e07 | ||
|
26960359a2 | ||
|
6ce208db95 | ||
|
ac1f9ac360 | ||
|
48ed516b5e | ||
|
b1b57d815a | ||
|
53023423b6 | ||
|
5b1451132d | ||
|
33c6a54414 | ||
|
bd553be10e | ||
|
67a0492c12 | ||
|
11dbde3bea | ||
|
55769e814f | ||
|
da9f41c295 | ||
|
62f95f2858 | ||
|
6cfcddac5a | ||
|
2308965658 | ||
|
97de26bf2e | ||
|
b552dcac24 | ||
|
63cfbbf59a | ||
|
e439bf3727 | ||
|
057b491176 | ||
|
6aa6aaaa8c | ||
|
1205e29bef | ||
|
d50955a173 | ||
|
965ebdee13 | ||
|
88f6ec458c | ||
|
08d0cce55a | ||
|
8ddad41bb7 | ||
|
1bec8e44b7 | ||
|
3026181b28 | ||
|
3c229b619e | ||
|
3c6733e121 | ||
|
f8b88fdc9a | ||
|
1f752b6cad | ||
|
48bfc395ee | ||
|
56f1db80cf | ||
|
17146f9b01 | ||
|
85976ffb1f | ||
|
26c4c48abc | ||
|
c00203499b | ||
|
d9c187ffc2 | ||
|
b320b94a73 | ||
|
40e9c66db3 | ||
|
5ff40d2d14 | ||
|
fb90cb78b3 | ||
|
b1f6d41209 | ||
|
680bf72871 | ||
|
d07642afe6 | ||
|
ee120c48e1 | ||
|
d804fe48f2 | ||
|
a987035ef3 | ||
|
2225ed62e1 | ||
|
ed4937f9df | ||
|
b0901104d8 | ||
|
0c90006520 | ||
|
079da60c8f | ||
|
72375b2d14 | ||
|
c79b12b5a3 | ||
|
9b41472922 | ||
|
709611b577 | ||
|
b0613dd0e4 | ||
|
5867331c7b | ||
|
b4e34a5794 | ||
|
07c51092e1 | ||
|
8990e173ac | ||
|
975f45ed1a | ||
|
10d22de98e | ||
|
894c564ab7 | ||
|
77b9832606 | ||
|
b8d8508d1f | ||
|
82d8dbc893 | ||
|
f33223daa7 | ||
|
9bb0554c94 | ||
|
b9f0e4f9ae | ||
|
dc5c971498 | ||
|
23b6419b42 | ||
|
657208054a | ||
|
fba6de41dd | ||
|
cfc8fea7f5 | ||
|
e1cfc0a80b | ||
|
481d3ff35d | ||
|
9d9a14c36d | ||
|
3483428f70 | ||
|
613e1e31f4 | ||
|
dd0d6ef28f | ||
|
b84f61afab | ||
|
63418666d9 | ||
|
0da06d4d54 | ||
|
cf9f59071e | ||
|
150008aae5 | ||
|
c8a56f8857 | ||
|
348fcf37a0 | ||
|
b8e598d66d | ||
|
13352216a4 | ||
|
aeb04d5b5c | ||
|
235096362f | ||
|
a12fc9299a | ||
|
7a14b77a41 | ||
|
6a19ab5a4b | ||
|
d1a9444401 | ||
|
b222caa901 | ||
|
11dafd4c48 | ||
|
4e1b82a8d8 | ||
|
bbf119cd3b | ||
|
83bc32bc9d | ||
|
6b8c5bdefc | ||
|
f3f0608606 | ||
|
280b38f804 | ||
|
1310509fa1 | ||
|
5c3199883f | ||
|
8ec456285b | ||
|
62ecbae614 | ||
|
08f67797d8 | ||
|
96c38b4349 | ||
|
de87573e0c | ||
|
b8a2d5953a | ||
|
399ad13a70 | ||
|
3dd04f68ab | ||
|
efe210f8ac | ||
|
f661cf8335 | ||
|
7940a7b954 | ||
|
45df7445f4 | ||
|
3d01acf169 | ||
|
b66a3f3479 | ||
|
968002f155 | ||
|
a73d653274 | ||
|
374d8a8a9e | ||
|
7e7f18ee0e | ||
|
e82a2d75c6 | ||
|
a818ec9017 | ||
|
3ce44908c9 | ||
|
e51e60209a | ||
|
def6b15e5b | ||
|
b085b5922a | ||
|
e17582e1a1 | ||
|
8d95596ffb | ||
|
5068ea05c3 | ||
|
e4cf59ecec | ||
|
a64ba65bef | ||
|
cd73cab699 | ||
|
0962db5aa6 | ||
|
4f21750fc2 | ||
|
fea9b5b194 | ||
|
4bbc0824a6 | ||
|
254e59c36f | ||
|
ac79b38cf0 | ||
|
1b8184ece1 | ||
|
9d245287a4 | ||
|
d5e71bc9be | ||
|
a04fff9d70 | ||
|
87aebdb630 | ||
|
930adb50ce | ||
|
c432d5875b | ||
|
11605dcdc6 | ||
|
6e137da469 | ||
|
59f4f134b5 | ||
|
b1a9293016 | ||
|
51adb3632b | ||
|
de98fb5812 | ||
|
224725039b | ||
|
b7e4937775 | ||
|
a2103c19f4 | ||
|
0988e0798a | ||
|
aa30750d39 | ||
|
9aabe2cbe6 | ||
|
3b541e3d05 | ||
|
470fb60dc5 | ||
|
40ae7e53ec | ||
|
54cc6a4b13 | ||
|
e2e496f505 | ||
|
692540cc78 | ||
|
91c1278d1a | ||
|
612ba5a4ea | ||
|
894b1053a2 | ||
|
6169346776 | ||
|
6e8dce95ae | ||
|
775d2e3523 | ||
|
44e9b0d0c5 | ||
|
96934c4614 | ||
|
fcc491fffb | ||
|
a9a8d672e9 | ||
|
b271b259bd | ||
|
83cc0d793b | ||
|
f617b2a9c2 | ||
|
3cde872e28 | ||
|
c90c27f364 | ||
|
ab551f4a15 | ||
|
94602c77c6 | ||
|
ce6e6dd6e1 | ||
|
67b21cb36f | ||
|
bad8e13282 | ||
|
d06217d203 | ||
|
fc49a50cc3 | ||
|
0916e03cb3 | ||
|
ddf8d24bf5 | ||
|
cb9eed6765 | ||
|
b0cba398a1 | ||
|
7690c2c002 | ||
|
1d1b9e8994 | ||
|
49aedf171a | ||
|
74263b2eb1 | ||
|
80125c3077 | ||
|
04fe299a6e | ||
|
2edf7f4ec0 | ||
|
884c4cce0c | ||
|
96908beaf7 | ||
|
ba64bceab0 | ||
|
e26a9f2a44 | ||
|
65d15cc3ef | ||
|
ce9d989bcd | ||
|
865c9535b9 | ||
|
2bf7e7f66d | ||
|
223d0b1d28 | ||
|
bd53940d23 | ||
|
df4a07982f | ||
|
c0fbed2110 | ||
|
15b5e6ae24 | ||
|
31468f5687 | ||
|
6f03597a5e | ||
|
0d178b38fb | ||
|
9a43f65b44 | ||
|
e6ad5887ca | ||
|
b49d315a39 | ||
|
05c89c19fb | ||
|
d720b47c8d | ||
|
2016011394 | ||
|
fb8a129752 | ||
|
095a213b42 | ||
|
6145197f44 | ||
|
651d57ee23 | ||
|
bba8377a82 | ||
|
2ae897187c | ||
|
6acd917cf7 | ||
|
29b8ea8398 | ||
|
a7ee042e9d | ||
|
090ce6e1f1 | ||
|
923d8282c7 | ||
|
0a5020bee1 | ||
|
fcc3de2a90 | ||
|
1ad0347479 | ||
|
8beadc759f | ||
|
3c11a980fe | ||
|
b491878c27 | ||
|
959e98eeb0 | ||
|
d2f8bf730b | ||
|
547a4cb576 | ||
|
d5d0c519ce | ||
|
a461e80ee4 | ||
|
83a2ad34cd | ||
|
980142dfcf | ||
|
36ac5626e9 | ||
|
132c4cb9d4 | ||
|
367c077a49 | ||
|
4d905a46ac | ||
|
5c191b9062 | ||
|
ea2cd9894f | ||
|
263c7267cb | ||
|
75bf398523 | ||
|
ae126a6dc3 | ||
|
6ce59aec1f | ||
|
297b807f96 | ||
|
87592392de | ||
|
ce8fa7f9f2 | ||
|
a7c1d0146a | ||
|
905b4249f3 | ||
|
4359e1f312 | ||
|
19ef9a094a | ||
|
f3144bf996 | ||
|
f76f38ef30 | ||
|
3d493c3bd9 | ||
|
2b08e59bea | ||
|
040f6aa10e | ||
|
e6b21594d7 | ||
|
85527e1f94 | ||
|
f4bbae29df | ||
|
f6dba3942c | ||
|
2ccd483ffc | ||
|
446013c752 | ||
|
21bad6105c | ||
|
a59f41b080 | ||
|
e45708b44e | ||
|
0314931f12 | ||
|
f8dff38e29 | ||
|
d8bdda94df | ||
|
9152075ed8 | ||
|
dc0c2900d1 | ||
|
14f156ef5f | ||
|
cb87f40165 | ||
|
7e8f6dbd4e | ||
|
e50e12bc9f | ||
|
24ebc20bf4 | ||
|
d1b734f07d | ||
|
9a5b3556d3 | ||
|
0d8f9db3fb | ||
|
3755850c2e | ||
|
219005952a | ||
|
84d82f82a9 | ||
|
40da74fc52 | ||
|
9351626ad8 | ||
|
1ba9652f38 | ||
|
52d14f3b8a | ||
|
a57098b1cb | ||
|
9cf52ca272 | ||
|
d4a9c2bb89 | ||
|
c1bd3c4c15 | ||
|
74b6cf9919 | ||
|
dc561c68c4 | ||
|
54ef52e1c6 | ||
|
96789b291f | ||
|
f0705e2c25 | ||
|
192ac963d9 | ||
|
1c4182fd5d | ||
|
06ca766c38 | ||
|
2f68424717 | ||
|
3e3584565f | ||
|
a97729d362 | ||
|
1755948768 | ||
|
26cfa88f3a | ||
|
763665f2f6 | ||
|
9812c58a86 | ||
|
9840a64dc9 | ||
|
701611c246 | ||
|
f0451476ad | ||
|
27a7c0e299 | ||
|
4e7b269b76 | ||
|
a9d819e1f7 | ||
|
be22f1639b | ||
|
b1948d70bd | ||
|
10d2f3eea7 | ||
|
364d4f8f41 | ||
|
546a82b7ec | ||
|
bb6da28008 | ||
|
a9e5beb9eb | ||
|
883d1778d5 | ||
|
7563045d75 | ||
|
205c177912 | ||
|
1e6b2a569e | ||
|
490ed929ad | ||
|
a65f786ba4 | ||
|
4f47ffdc8d | ||
|
33c6fdf5ab | ||
|
2a05ac7ed1 | ||
|
43c0c99136 | ||
|
ea2d60d3df | ||
|
b331319aea | ||
|
b1245fed16 | ||
|
a480a39713 | ||
|
70dffa94e7 | ||
|
d2c1e7c87b | ||
|
c32340662c | ||
|
9cdd95c114 | ||
|
fdb7d5a253 | ||
|
8151fe75b8 | ||
|
e9a40f2ae9 | ||
|
134d6c6d32 | ||
|
34ddc6ce85 | ||
|
34f5978446 | ||
|
4d41160379 | ||
|
e05fbcc1b3 | ||
|
23eb8eea80 | ||
|
bc713f22cb | ||
|
d98d2a6a73 | ||
|
e12c329507 | ||
|
e51f6070df | ||
|
3793ce17c0 | ||
|
dbd3957a9f | ||
|
5b17e98217 | ||
|
c0edb0c927 | ||
|
41f931c7a0 | ||
|
12b022a49a | ||
|
94ee97085f | ||
|
adde400893 | ||
|
91a99ee39c | ||
|
c585982a1a | ||
|
5a92330164 | ||
|
bfcfccfc8d | ||
|
da3bb5eea8 | ||
|
1bf5da2a8d | ||
|
bc1d47efb5 | ||
|
0b103417f9 | ||
|
55dc65753e | ||
|
e827b10e7b | ||
|
fd3faf04e5 | ||
|
3051374b46 | ||
|
be4a6231ae | ||
|
b81e961c86 | ||
|
3682fe0318 | ||
|
2cb3d4b68e | ||
|
cf35f7c76c | ||
|
bb2d24ec27 | ||
|
6a152820ca | ||
|
53546a27de | ||
|
0dddea534f | ||
|
167d0a3964 | ||
|
e05f069b3b | ||
|
961404f397 | ||
|
ea4ddc7f79 | ||
|
76a1d43e66 | ||
|
1352d4e02b | ||
|
7f8877fba5 | ||
|
fe35baa627 | ||
|
c73b6e32a9 | ||
|
c952ef1a88 | ||
|
f31db13cd0 | ||
|
6e1d340c87 | ||
|
28cccb49f7 | ||
|
b54e02c9ba | ||
|
6da95d72ea | ||
|
51f794edb6 | ||
|
616c549d2c | ||
|
29935c2d33 | ||
|
de59d26968 | ||
|
65e63026a0 | ||
|
6008cb6de4 | ||
|
d2db5f5584 | ||
|
f8a5860f34 | ||
|
61829abc8c | ||
|
f687406ac5 | ||
|
489d2878da | ||
|
384bff08e8 | ||
|
d314d1041f | ||
|
62c0694901 | ||
|
73706fa6bb | ||
|
443b5eaafc | ||
|
24706f50d4 | ||
|
c3938d1e4f | ||
|
6d7a0d053d | ||
|
748dc4c509 | ||
|
d6dbe2a4a8 | ||
|
40610c64a3 | ||
|
3aeee3af3d | ||
|
8a3ba7a321 | ||
|
c63dbea34b | ||
|
d8a4b4a0fd | ||
|
90d84e8be9 | ||
|
d7c160c3b0 | ||
|
31679df08c | ||
|
70a6c72751 | ||
|
5661db88d1 | ||
|
14dca8297d | ||
|
5a546689b9 | ||
|
670437d756 | ||
|
b19308b1c2 | ||
|
6cfd5b2854 | ||
|
0a660e5e56 | ||
|
568073a090 | ||
|
c5774a93fe | ||
|
345fe877c4 | ||
|
c72e7e55eb | ||
|
3f08ed629c | ||
|
1db1cbdd9c | ||
|
3d30038795 | ||
|
75e11a8195 | ||
|
bc59d0ad8e | ||
|
9e26aaf6ad | ||
|
95e28ba3ef | ||
|
b8fb64d893 | ||
|
dfa0a06cce | ||
|
94e2a5c49f | ||
|
e666d849be | ||
|
e05f41af40 | ||
|
ec2cea1afa | ||
|
bc15439e3d | ||
|
f0f117663c | ||
|
e7391937dd | ||
|
a3f3d90ab7 | ||
|
4013f78ecb | ||
|
6d0837e7cb | ||
|
5491e2cb00 | ||
|
bbcd69967c | ||
|
b5e7614b05 | ||
|
2a39f5d8ea | ||
|
00b5a176d3 | ||
|
2600f0f041 | ||
|
1d76db8164 | ||
|
69cec060d8 | ||
|
bf7ad7eeda | ||
|
463ce68b16 | ||
|
666ddb2ff3 | ||
|
f5083cb190 | ||
|
507246468b | ||
|
4f904f4700 | ||
|
80a06a0354 | ||
|
26a4d315c5 | ||
|
f034946cda | ||
|
459d7df42b | ||
|
73905547c8 | ||
|
be20450a66 | ||
|
4bdcf054eb |
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
blank_issues_enabled: false
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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
22
.github/stale.yml
vendored
Normal 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
31
.github/workflows/deploy.yml
vendored
Normal 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
26
.github/workflows/lint.yml
vendored
Normal 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
43
.github/workflows/tests.yml
vendored
Normal 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
10
.gitignore
vendored
|
@ -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
23
.pre-commit-config.yaml
Normal 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
18
.readthedocs.yaml
Normal 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
32
.ruff.toml
Normal 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
|
56
.travis.yml
56
.travis.yml
|
@ -1,56 +0,0 @@
|
|||
language: python
|
||||
sudo: false
|
||||
python:
|
||||
- 2.7
|
||||
- 3.4
|
||||
- 3.5
|
||||
- 3.6
|
||||
install:
|
||||
- |
|
||||
if [ "$TEST_TYPE" = build ]; then
|
||||
pip install -e .[test]
|
||||
pip install psycopg2 # Required for Django postgres fields testing
|
||||
pip install django==$DJANGO_VERSION
|
||||
if [ $DJANGO_VERSION = 1.8 ]; then # DRF dropped 1.8 support at 3.7.0
|
||||
pip install djangorestframework==3.6.4
|
||||
fi
|
||||
python setup.py develop
|
||||
elif [ "$TEST_TYPE" = lint ]; then
|
||||
pip install flake8
|
||||
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: '2.7'
|
||||
env: TEST_TYPE=build DJANGO_VERSION=1.8
|
||||
- python: '2.7'
|
||||
env: TEST_TYPE=build DJANGO_VERSION=1.9
|
||||
- python: '2.7'
|
||||
env: TEST_TYPE=build DJANGO_VERSION=1.10
|
||||
- python: '2.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"
|
76
CODE_OF_CONDUCT.md
Normal file
76
CODE_OF_CONDUCT.md
Normal file
|
@ -0,0 +1,76 @@
|
|||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at me@syrusakbary.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
62
CONTRIBUTING.md
Normal file
62
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,62 @@
|
|||
# Contributing
|
||||
|
||||
Thanks for helping to make graphene-django great!
|
||||
|
||||
We welcome all kinds of contributions:
|
||||
|
||||
- Bug fixes
|
||||
- Documentation improvements
|
||||
- New features
|
||||
- Refactoring & tidying
|
||||
|
||||
|
||||
## Getting started
|
||||
|
||||
If you have a specific contribution in mind, be sure to check the [issues](https://github.com/graphql-python/graphene-django/issues) and [projects](https://github.com/graphql-python/graphene-django/projects) in progress - someone could already be working on something similar and you can help out.
|
||||
|
||||
|
||||
## Project setup
|
||||
|
||||
After cloning this repo, ensure dependencies are installed by running:
|
||||
|
||||
```sh
|
||||
make dev-setup
|
||||
```
|
||||
|
||||
## Running tests
|
||||
|
||||
After developing, the full test suite can be evaluated by running:
|
||||
|
||||
```sh
|
||||
make tests
|
||||
```
|
||||
|
||||
## Opening Pull Requests
|
||||
|
||||
Please fork the project and open a pull request against the `main` branch.
|
||||
|
||||
This will trigger a series of test and lint checks.
|
||||
|
||||
We advise that you format and run lint locally before doing this to save time:
|
||||
|
||||
```sh
|
||||
make format
|
||||
make lint
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
The [documentation](http://docs.graphene-python.org/projects/django/en/latest/) is generated using the excellent [Sphinx](http://www.sphinx-doc.org/) and a custom theme.
|
||||
|
||||
The documentation dependencies are installed by running:
|
||||
|
||||
```sh
|
||||
cd docs
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Then to produce a HTML version of the documentation:
|
||||
|
||||
```sh
|
||||
make html
|
||||
```
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016-Present Syrus Akbary
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -1,2 +1,6 @@
|
|||
include README.md
|
||||
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
|
||||
|
|
29
Makefile
Normal file
29
Makefile
Normal file
|
@ -0,0 +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:
|
||||
PYTHONPATH=. pytest graphene_django --cov=graphene_django -vv
|
||||
|
||||
.PHONY: format ## Format code
|
||||
format:
|
||||
ruff format graphene_django examples setup.py
|
||||
|
||||
.PHONY: lint ## Lint code
|
||||
lint:
|
||||
ruff graphene_django examples
|
||||
|
||||
.PHONY: docs ## Generate docs
|
||||
docs: dev-setup
|
||||
cd docs && make install && make html
|
||||
|
||||
.PHONY: docs-live ## Generate docs with live reloading
|
||||
docs-live: dev-setup
|
||||
cd docs && make install && make livehtml
|
176
README.md
176
README.md
|
@ -1,127 +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-Django
|
||||
|
||||
---
|
||||
[![build][build-image]][build-url]
|
||||
[![pypi][pypi-image]][pypi-url]
|
||||
[![Anaconda-Server Badge][conda-image]][conda-url]
|
||||
[![coveralls][coveralls-image]][coveralls-url]
|
||||
|
||||
#  Graphene-Django [](https://travis-ci.org/graphql-python/graphene-django) [](https://badge.fury.io/py/graphene-django) [](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
|
||||
|
||||
* Seamless integration with Django models
|
||||
* Automatic generation of GraphQL schema
|
||||
* Integration with Django's authentication and permission system
|
||||
* Easy querying and filtering of data
|
||||
* Support for Django's pagination system
|
||||
* Compatible with Django's form and validation system
|
||||
* Extensive documentation and community support
|
||||
|
||||
## Installation
|
||||
|
||||
For instaling graphene, just run this command in your shell
|
||||
To install Graphene-Django, run the following command:
|
||||
|
||||
```bash
|
||||
pip install "graphene-django>=2.0.dev"
|
||||
```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 = [
|
||||
# ...
|
||||
'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)
|
||||
|
||||
@graphene.resolve_only_args
|
||||
def resolve_users(self):
|
||||
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
|
||||
|
||||
After cloning this repo, ensure dependencies are installed by running:
|
||||
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).
|
||||
|
||||
```sh
|
||||
pip install -e ".[test]"
|
||||
```
|
||||
## License
|
||||
|
||||
After developing, the full test suite can be evaluated by running:
|
||||
Graphene-Django is released under the [MIT License](https://github.com/graphql-python/graphene-django/blob/main/LICENSE).
|
||||
|
||||
```sh
|
||||
py.test graphene_django --cov=graphene_django # Use -v -s for verbose mode
|
||||
```
|
||||
## 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
|
||||
|
||||
### Documentation
|
||||
## Tutorials and Examples
|
||||
|
||||
The [documentation](http://docs.graphene-python.org/projects/django/en/latest/) is generated using the excellent [Sphinx](http://www.sphinx-doc.org/) and a custom theme.
|
||||
* [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)
|
||||
|
||||
The documentation dependencies are installed by running:
|
||||
## Related Projects
|
||||
|
||||
```sh
|
||||
cd docs
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
* [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
|
||||
|
||||
Then to produce a HTML version of the documentation:
|
||||
## Support
|
||||
|
||||
```sh
|
||||
make html
|
||||
```
|
||||
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)
|
||||
|
|
145
README.rst
145
README.rst
|
@ -1,145 +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/>`__.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
For instaling graphene, just run this command in your shell
|
||||
|
||||
.. code:: bash
|
||||
|
||||
pip install "graphene-django>=2.0.dev"
|
||||
|
||||
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
|
||||
------------
|
||||
|
||||
After cloning this repo, ensure dependencies are installed by running:
|
||||
|
||||
.. code:: sh
|
||||
|
||||
pip install -e ".[test]"
|
||||
|
||||
After developing, the full test suite can be evaluated by running:
|
||||
|
||||
.. code:: sh
|
||||
|
||||
py.test graphene_django --cov=graphene_django # Use -v -s for verbose mode
|
||||
|
||||
Documentation
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
The `documentation <http://docs.graphene-python.org/projects/django/en/latest/>`__ is generated using the excellent
|
||||
`Sphinx <http://www.sphinx-doc.org/>`__ and a custom theme.
|
||||
|
||||
The documentation dependencies are installed by running:
|
||||
|
||||
.. code:: sh
|
||||
|
||||
cd docs
|
||||
pip install -r requirements.txt
|
||||
|
||||
Then to produce a HTML version of the documentation:
|
||||
|
||||
.. code:: sh
|
||||
|
||||
make html
|
||||
|
||||
.. |Graphene Logo| image:: http://graphene-python.org/favicon.png
|
||||
.. |Build Status| image:: https://travis-ci.org/graphql-python/graphene-django.svg?branch=master
|
||||
:target: https://travis-ci.org/graphql-python/graphene-django
|
||||
.. |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
|
|
@ -1,34 +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.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'
|
|
@ -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
0
docs/_static/.gitkeep
vendored
Normal file
|
@ -20,7 +20,7 @@ Let's use a simple example model.
|
|||
Limiting Field Access
|
||||
---------------------
|
||||
|
||||
This is easy, 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 @@ This is easy, 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 atrribute.
|
||||
conversely you can use ``exclude`` meta attribute.
|
||||
|
||||
.. code:: python
|
||||
|
||||
|
@ -45,9 +45,34 @@ conversely you can use ``exclude_fields`` meta atrribute.
|
|||
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
|
||||
---------------------------
|
||||
|
||||
|
@ -61,10 +86,11 @@ define a resolve method for that field and return the desired queryset.
|
|||
from .models import Post
|
||||
|
||||
class Query(ObjectType):
|
||||
all_posts = DjangoFilterConnectionField(CategoryNode)
|
||||
all_posts = DjangoFilterConnectionField(PostNode)
|
||||
|
||||
def resolve_all_posts(self, info):
|
||||
return Post.objects.filter(published=True)
|
||||
|
||||
def resolve_all_posts(self, args, info):
|
||||
return Post.objects.filter(published=True)
|
||||
|
||||
User-based Queryset Filtering
|
||||
-----------------------------
|
||||
|
@ -79,11 +105,11 @@ with the context argument.
|
|||
from .models import Post
|
||||
|
||||
class Query(ObjectType):
|
||||
my_posts = DjangoFilterConnectionField(CategoryNode)
|
||||
my_posts = DjangoFilterConnectionField(PostNode)
|
||||
|
||||
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)
|
||||
|
@ -95,7 +121,46 @@ schema is simple.
|
|||
|
||||
result = schema.execute(query, context_value=request)
|
||||
|
||||
Filtering ID-based node access
|
||||
|
||||
Global Filtering
|
||||
----------------
|
||||
|
||||
If you are using ``DjangoObjectType`` you can define a custom `get_queryset`.
|
||||
|
||||
.. code:: python
|
||||
|
||||
from graphene import relay
|
||||
from graphene_django.types import DjangoObjectType
|
||||
from .models import Post
|
||||
|
||||
class PostNode(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Post
|
||||
fields = '__all__'
|
||||
|
||||
@classmethod
|
||||
def get_queryset(cls, queryset, info):
|
||||
if info.context.user.is_anonymous:
|
||||
return queryset.filter(published=True)
|
||||
return queryset
|
||||
|
||||
.. warning::
|
||||
|
||||
Defining a custom ``get_queryset`` gives the guaranteed it will be called
|
||||
when resolving the ``DjangoObjectType``, even through related objects.
|
||||
Note that because of this, benefits from using ``select_related``
|
||||
in objects that define a relation to this ``DjangoObjectType`` will be canceled out.
|
||||
In the case of ``prefetch_related``, the benefits of the optimization will be lost only
|
||||
if the custom ``get_queryset`` modifies the queryset. For more information about this, refers
|
||||
to Django documentation about ``prefetch_related``: https://docs.djangoproject.com/en/4.2/ref/models/querysets/#prefetch-related.
|
||||
|
||||
|
||||
If you want to explicitly disable the execution of the custom ``get_queryset`` when resolving,
|
||||
you can decorate the resolver with `@graphene_django.bypass_get_queryset`. Note that this
|
||||
can lead to authorization leaks if you are performing authorization checks in the custom
|
||||
``get_queryset``.
|
||||
|
||||
Filtering ID-based Node Access
|
||||
------------------------------
|
||||
|
||||
In order to add authorization to id-based node access, we need to add a
|
||||
|
@ -109,27 +174,30 @@ method to your ``DjangoObjectType``.
|
|||
class PostNode(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Post
|
||||
only_fields = ('title', 'content')
|
||||
fields = ('title', 'content')
|
||||
interfaces = (relay.Node, )
|
||||
|
||||
@classmethod
|
||||
def get_node(cls, id, context, info):
|
||||
def get_node(cls, info, id):
|
||||
try:
|
||||
post = cls._meta.model.objects.get(id=id)
|
||||
except cls._meta.model.DoesNotExist:
|
||||
return None
|
||||
|
||||
if post.published or context.user == post.owner:
|
||||
if post.published or info.context.user == post.owner:
|
||||
return post
|
||||
return None
|
||||
|
||||
Adding login required
|
||||
|
||||
Adding Login Required
|
||||
---------------------
|
||||
|
||||
If you want to use the standard Django LoginRequiredMixin_ you can create your own view, which includes the ``LoginRequiredMixin`` and subclasses the ``GraphQLView``:
|
||||
To restrict users from accessing the GraphQL API page the standard Django LoginRequiredMixin_ can be used to create your own standard Django Class Based View, which includes the ``LoginRequiredMixin`` and subclasses the ``GraphQLView``.:
|
||||
|
||||
.. code:: python
|
||||
|
||||
# views.py
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from graphene_django.views import GraphQLView
|
||||
|
||||
|
@ -137,13 +205,15 @@ If you want to use the standard Django LoginRequiredMixin_ you can create your o
|
|||
class PrivateGraphQLView(LoginRequiredMixin, GraphQLView):
|
||||
pass
|
||||
|
||||
After this, you can use the new ``PrivateGraphQLView`` in ``urls.py``:
|
||||
After this, you can use the new ``PrivateGraphQLView`` in the project's URL Configuration file ``url.py``:
|
||||
|
||||
For Django 2.2 and above:
|
||||
|
||||
.. code:: python
|
||||
|
||||
urlpatterns = [
|
||||
# some other urls
|
||||
url(r'^graphql', PrivateGraphQLView.as_view(graphiql=True, schema=schema)),
|
||||
# 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
|
||||
|
|
94
docs/conf.py
94
docs/conf.py
|
@ -1,6 +1,6 @@
|
|||
import os
|
||||
|
||||
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
|
||||
on_rtd = os.environ.get("READTHEDOCS", None) == "True"
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
@ -34,53 +34,51 @@ on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
|
|||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.coverage',
|
||||
'sphinx.ext.viewcode',
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.intersphinx",
|
||||
"sphinx.ext.todo",
|
||||
"sphinx.ext.coverage",
|
||||
"sphinx.ext.viewcode",
|
||||
]
|
||||
if not on_rtd:
|
||||
extensions += [
|
||||
'sphinx.ext.githubpages',
|
||||
]
|
||||
extensions += ["sphinx.ext.githubpages"]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
templates_path = ["_templates"]
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
#
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.rst'
|
||||
source_suffix = ".rst"
|
||||
|
||||
# The encoding of source files.
|
||||
#
|
||||
# source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
master_doc = "index"
|
||||
|
||||
# General information about the project.
|
||||
project = u'Graphene Django'
|
||||
copyright = u'Graphene 2017'
|
||||
author = u'Syrus Akbary'
|
||||
project = "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:
|
||||
|
@ -94,7 +92,7 @@ language = None
|
|||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This patterns also effect to html_static_path and html_extra_path
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
|
@ -116,7 +114,7 @@ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
|||
# show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
pygments_style = "sphinx"
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
# modindex_common_prefix = []
|
||||
|
@ -175,7 +173,7 @@ html_theme_path = [sphinx_graphene_theme.get_html_theme_path()]
|
|||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
html_static_path = ["_static"]
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
|
@ -255,34 +253,30 @@ html_static_path = ['_static']
|
|||
# html_search_scorer = 'scorer.js'
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'Graphenedoc'
|
||||
htmlhelp_basename = "Graphenedoc"
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'Graphene.tex', u'Graphene Documentation',
|
||||
u'Syrus Akbary', 'manual'),
|
||||
(master_doc, "Graphene.tex", "Graphene Documentation", "Syrus Akbary", "manual")
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
|
@ -323,8 +317,7 @@ latex_documents = [
|
|||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'graphene_django', u'Graphene Django Documentation',
|
||||
[author], 1)
|
||||
(master_doc, "graphene_django", "Graphene Django Documentation", [author], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
|
@ -338,9 +331,15 @@ man_pages = [
|
|||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'Graphene-Django', u'Graphene Django Documentation',
|
||||
author, 'Graphene Django', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
(
|
||||
master_doc,
|
||||
"Graphene-Django",
|
||||
"Graphene Django Documentation",
|
||||
author,
|
||||
"Graphene Django",
|
||||
"One line description of project.",
|
||||
"Miscellaneous",
|
||||
)
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
|
@ -414,7 +413,7 @@ epub_copyright = copyright
|
|||
# epub_post_files = []
|
||||
|
||||
# A list of files that should not be packed into the epub file.
|
||||
epub_exclude_files = ['search.html']
|
||||
epub_exclude_files = ["search.html"]
|
||||
|
||||
# The depth of the table of contents in toc.ncx.
|
||||
#
|
||||
|
@ -446,4 +445,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),
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
@ -15,7 +15,7 @@ For use the Django Debug plugin in Graphene:
|
|||
|
||||
* Add ``graphene_django.debug.DjangoDebugMiddleware`` into ``MIDDLEWARE`` in the ``GRAPHENE`` settings.
|
||||
|
||||
* Add the ``debug`` field into the schema root ``Query`` with the value ``graphene.Field(DjangoDebug, name='__debug')``.
|
||||
* Add the ``debug`` field into the schema root ``Query`` with the value ``graphene.Field(DjangoDebug, name='_debug')``.
|
||||
|
||||
|
||||
.. code:: python
|
||||
|
@ -24,7 +24,7 @@ For use the Django Debug plugin in Graphene:
|
|||
|
||||
class Query(graphene.ObjectType):
|
||||
# ...
|
||||
debug = graphene.Field(DjangoDebug, name='__debug')
|
||||
debug = graphene.Field(DjangoDebug, name='_debug')
|
||||
|
||||
schema = graphene.Schema(query=Query)
|
||||
|
||||
|
@ -34,6 +34,7 @@ And in your ``settings.py``:
|
|||
.. code:: python
|
||||
|
||||
GRAPHENE = {
|
||||
...
|
||||
'MIDDLEWARE': [
|
||||
'graphene_django.debug.DjangoDebugMiddleware',
|
||||
]
|
||||
|
@ -42,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::
|
||||
|
@ -58,11 +59,15 @@ the GraphQL request, like:
|
|||
}
|
||||
}
|
||||
# Here is the debug field that will output the SQL queries
|
||||
__debug {
|
||||
_debug {
|
||||
sql {
|
||||
rawSql
|
||||
}
|
||||
exceptions {
|
||||
message
|
||||
stack
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Note that the ``__debug`` field must be the last field in your query.
|
||||
Note that the ``_debug`` field must be the last field in your query.
|
||||
|
|
12
docs/extra-types.rst
Normal file
12
docs/extra-types.rst
Normal 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
85
docs/fields.rst
Normal 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`.
|
|
@ -2,9 +2,8 @@ Filtering
|
|||
=========
|
||||
|
||||
Graphene integrates with
|
||||
`django-filter <https://django-filter.readthedocs.org>`__ to provide
|
||||
filtering of results. See the `usage
|
||||
documentation <https://django-filter.readthedocs.io/en/latest/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
|
||||
pip install 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/latest/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
|
||||
|
@ -126,3 +146,198 @@ create your own ``Filterset`` as follows:
|
|||
# We specify our custom AnimalFilter using the filterset_class param
|
||||
all_animals = DjangoFilterConnectionField(AnimalNode,
|
||||
filterset_class=AnimalFilter)
|
||||
|
||||
|
||||
If you were interested in selecting all dogs and cats, you might query as follows:
|
||||
|
||||
.. code::
|
||||
|
||||
query {
|
||||
allAnimals(genera: ["Canis", "Panthera"]) {
|
||||
edges {
|
||||
node {
|
||||
id,
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
You can also specify the ``FilterSet`` class using the ``filterset_class``
|
||||
parameter when defining your ``DjangoObjectType``, however, this can't be used
|
||||
in unison with the ``filter_fields`` parameter:
|
||||
|
||||
.. code:: python
|
||||
|
||||
class AnimalFilter(django_filters.FilterSet):
|
||||
# Do case-insensitive lookups on 'name'
|
||||
name = django_filters.CharFilter(lookup_expr=['iexact'])
|
||||
|
||||
class Meta:
|
||||
# Assume you have an Animal model defined with the following fields
|
||||
model = Animal
|
||||
fields = ['name', 'genus', 'is_domesticated']
|
||||
|
||||
|
||||
class AnimalNode(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Animal
|
||||
fields = '__all__'
|
||||
filterset_class = AnimalFilter
|
||||
interfaces = (relay.Node, )
|
||||
|
||||
|
||||
class Query(ObjectType):
|
||||
animal = relay.Node.Field(AnimalNode)
|
||||
all_animals = DjangoFilterConnectionField(AnimalNode)
|
||||
|
||||
|
||||
The context argument is passed on as the `request argument <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``).
|
||||
|
||||
.. code:: python
|
||||
|
||||
class AnimalFilter(django_filters.FilterSet):
|
||||
# Do case-insensitive lookups on 'name'
|
||||
name = django_filters.CharFilter(lookup_type=['iexact'])
|
||||
|
||||
class Meta:
|
||||
model = Animal
|
||||
fields = ['name', 'genus', 'is_domesticated']
|
||||
|
||||
@property
|
||||
def qs(self):
|
||||
# The query context can be found in self.request.
|
||||
return super(AnimalFilter, self).qs.filter(owner=self.request.user)
|
||||
|
||||
|
||||
Ordering
|
||||
--------
|
||||
|
||||
You can use ``OrderFilter`` to define how you want your returned results to be ordered.
|
||||
|
||||
Extend the tuple of fields if you want to order by more than one field.
|
||||
|
||||
.. code:: python
|
||||
|
||||
from django_filters import FilterSet, OrderingFilter
|
||||
|
||||
class UserFilter(FilterSet):
|
||||
class Meta:
|
||||
model = UserModel
|
||||
|
||||
order_by = OrderingFilter(
|
||||
fields=(
|
||||
('name', 'created_at'),
|
||||
)
|
||||
)
|
||||
|
||||
class Group(DjangoObjectType):
|
||||
users = DjangoFilterConnectionField(Ticket, filterset_class=UserFilter)
|
||||
|
||||
class Meta:
|
||||
name = 'Group'
|
||||
model = GroupModel
|
||||
fields = '__all__'
|
||||
interfaces = (relay.Node,)
|
||||
|
||||
def resolve_users(self, info, **kwargs):
|
||||
return UserFilter(kwargs).qs
|
||||
|
||||
|
||||
with this set up, you can now order the users under group:
|
||||
|
||||
.. code::
|
||||
|
||||
query {
|
||||
group(id: "xxx") {
|
||||
users(orderBy: "-created_at") {
|
||||
xxx
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
PostgreSQL `ArrayField`
|
||||
-----------------------
|
||||
|
||||
Graphene provides an easy to implement filters on `ArrayField` as they are not natively supported by django_filters:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from django.db import models
|
||||
from django_filters import FilterSet, OrderingFilter
|
||||
from graphene_django.filter import ArrayFilter
|
||||
|
||||
class Event(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
tags = ArrayField(models.CharField(max_length=50))
|
||||
|
||||
class EventFilterSet(FilterSet):
|
||||
class Meta:
|
||||
model = Event
|
||||
fields = {
|
||||
"name": ["exact", "contains"],
|
||||
}
|
||||
|
||||
tags__contains = ArrayFilter(field_name="tags", lookup_expr="contains")
|
||||
tags__overlap = ArrayFilter(field_name="tags", lookup_expr="overlap")
|
||||
tags = ArrayFilter(field_name="tags", lookup_expr="exact")
|
||||
|
||||
class EventType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Event
|
||||
interfaces = (Node,)
|
||||
fields = "__all__"
|
||||
filterset_class = EventFilterSet
|
||||
|
||||
with this set up, you can now filter events by tags:
|
||||
|
||||
.. code::
|
||||
|
||||
query {
|
||||
events(tags_Overlap: ["concert", "festival"]) {
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
`TypedFilter`
|
||||
-------------
|
||||
|
||||
Sometimes the automatic detection of the filter input type is not satisfactory for what you are trying to achieve.
|
||||
You can then explicitly specify the input type you want for your filter by using a `TypedFilter`:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from django.db import models
|
||||
from django_filters import FilterSet, OrderingFilter
|
||||
import graphene
|
||||
from graphene_django.filter import TypedFilter
|
||||
|
||||
class Event(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
|
||||
class EventFilterSet(FilterSet):
|
||||
class Meta:
|
||||
model = Event
|
||||
fields = {
|
||||
"name": ["exact", "contains"],
|
||||
}
|
||||
|
||||
only_first = TypedFilter(input_type=graphene.Boolean, method="only_first_filter")
|
||||
|
||||
def only_first_filter(self, queryset, _name, value):
|
||||
if value:
|
||||
return queryset[:1]
|
||||
else:
|
||||
return queryset
|
||||
|
||||
class EventType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Event
|
||||
interfaces = (Node,)
|
||||
fields = "__all__"
|
||||
filterset_class = EventFilterSet
|
||||
|
|
|
@ -1,15 +1,38 @@
|
|||
Graphene-Django
|
||||
===============
|
||||
|
||||
Contents:
|
||||
Welcome to the Graphene-Django docs.
|
||||
|
||||
Graphene-Django is built on top of `Graphene <https://docs.graphene-python.org/en/latest/>`__.
|
||||
Graphene-Django provides some additional abstractions that make it easy to add GraphQL functionality to your Django project.
|
||||
|
||||
First time? We recommend you start with the installation guide to get set up and the basic tutorial.
|
||||
It is worth reading the `core graphene docs <https://docs.graphene-python.org/en/latest/>`__ to familiarize yourself with the basic utilities.
|
||||
|
||||
Core tenets
|
||||
-----------
|
||||
|
||||
If you want to expose your data through GraphQL - read the ``Installation``, ``Schema`` and ``Queries`` section.
|
||||
|
||||
|
||||
For more advanced use, check out the Relay tutorial.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 0
|
||||
:maxdepth: 1
|
||||
|
||||
installation
|
||||
tutorial-plain
|
||||
tutorial-relay
|
||||
schema
|
||||
queries
|
||||
fields
|
||||
extra-types
|
||||
mutations
|
||||
subscriptions
|
||||
filtering
|
||||
authorization
|
||||
debug
|
||||
rest-framework
|
||||
introspection
|
||||
validation
|
||||
testing
|
||||
settings
|
||||
|
|
93
docs/installation.rst
Normal file
93
docs/installation.rst
Normal file
|
@ -0,0 +1,93 @@
|
|||
Installation
|
||||
============
|
||||
|
||||
Graphene-Django takes a few seconds to install and set up.
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
Graphene-Django currently supports the following versions of Django:
|
||||
|
||||
* >= Django 2.2
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
.. code:: bash
|
||||
|
||||
pip install graphene-django
|
||||
|
||||
**We strongly recommend pinning against a specific version of Graphene-Django because new versions could introduce breaking changes to your project.**
|
||||
|
||||
Add ``graphene_django`` to the ``INSTALLED_APPS`` in the ``settings.py`` file of your Django project:
|
||||
|
||||
.. code:: python
|
||||
|
||||
INSTALLED_APPS = [
|
||||
...
|
||||
"django.contrib.staticfiles", # Required for GraphiQL
|
||||
"graphene_django"
|
||||
]
|
||||
|
||||
|
||||
We need to add a ``graphql`` URL to the ``urls.py`` of your Django project:
|
||||
|
||||
For Django 2.2 and above:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from django.urls import path
|
||||
from graphene_django.views import GraphQLView
|
||||
|
||||
urlpatterns = [
|
||||
# ...
|
||||
path("graphql", GraphQLView.as_view(graphiql=True)),
|
||||
]
|
||||
|
||||
(Change ``graphiql=True`` to ``graphiql=False`` if you do not want to use the GraphiQL API browser.)
|
||||
|
||||
Finally, define the schema location for Graphene in the ``settings.py`` file of your Django project:
|
||||
|
||||
.. code:: python
|
||||
|
||||
GRAPHENE = {
|
||||
"SCHEMA": "django_root.schema.schema"
|
||||
}
|
||||
|
||||
Where ``path.schema.schema`` is the location of the ``Schema`` object in your Django project.
|
||||
|
||||
The most basic ``schema.py`` looks like this:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import graphene
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
hello = graphene.String(default_value="Hi!")
|
||||
|
||||
schema = graphene.Schema(query=Query)
|
||||
|
||||
|
||||
To learn how to extend the schema object for your project, read the basic tutorial.
|
||||
|
||||
CSRF exempt
|
||||
-----------
|
||||
|
||||
If you have enabled `CSRF protection <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))),
|
||||
]
|
|
@ -1,17 +1,15 @@
|
|||
Introspection Schema
|
||||
====================
|
||||
|
||||
Relay uses `Babel Relay
|
||||
Plugin <https://facebook.github.io/relay/docs/guides-babel-plugin.html>`__
|
||||
that requires you to provide your GraphQL schema data.
|
||||
Relay Modern uses `Babel Relay Plugin <https://facebook.github.io/relay/docs/en/installation-and-setup>`__ which requires you to provide your GraphQL schema data.
|
||||
|
||||
Graphene comes with a management command for Django to dump your schema
|
||||
data to ``schema.json`` that is compatible with babel-relay-plugin.
|
||||
Graphene comes with a Django management command to dump your schema
|
||||
data to ``schema.json`` which is compatible with babel-relay-plugin.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
Include ``graphene_django`` to ``INSTALLED_APPS`` in you project
|
||||
Include ``graphene_django`` to ``INSTALLED_APPS`` in your project
|
||||
settings:
|
||||
|
||||
.. code:: python
|
||||
|
@ -29,20 +27,39 @@ It dumps your full introspection schema to ``schema.json`` inside your
|
|||
project root directory. Point ``babel-relay-plugin`` to this file and
|
||||
you're ready to use Relay with Graphene GraphQL implementation.
|
||||
|
||||
The schema file is sorted to create a reproducible canonical representation.
|
||||
|
||||
GraphQL SDL Representation
|
||||
--------------------------
|
||||
|
||||
The schema can also be exported as a GraphQL SDL file by changing the file
|
||||
extension :
|
||||
|
||||
.. code:: bash
|
||||
|
||||
./manage.py graphql_schema --schema tutorial.quickstart.schema --out schema.graphql
|
||||
|
||||
When exporting the schema as a ``.graphql`` file the ``--indent`` option is
|
||||
ignored.
|
||||
|
||||
|
||||
Advanced Usage
|
||||
--------------
|
||||
|
||||
The ``--indent`` option can be used to specify the number of indentation spaces to
|
||||
be used in the output. Defaults to `None` which displays all data on a single line.
|
||||
|
||||
The ``--watch`` option can be used to run ``./manage.py graphql_schema`` in watch mode, where it will automatically output a new schema every time there are file changes in your project
|
||||
|
||||
To simplify the command to ``./manage.py graphql_schema``, you can
|
||||
specify the parameters in your settings.py:
|
||||
|
||||
.. code:: python
|
||||
|
||||
GRAPHENE = {
|
||||
'SCHEMA': 'tutorial.quickstart.schema',
|
||||
'SCHEMA_OUTPUT': 'data/schema.json' # defaults to schema.json
|
||||
'SCHEMA': '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)
|
||||
}
|
||||
|
||||
|
||||
|
|
401
docs/mutations.rst
Normal file
401
docs/mutations.rst
Normal file
|
@ -0,0 +1,401 @@
|
|||
Mutations
|
||||
=========
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
Graphene-Django makes it easy to perform mutations.
|
||||
|
||||
With Graphene-Django we can take advantage of pre-existing Django features to
|
||||
quickly build CRUD functionality, while still using the core `graphene mutation <https://docs.graphene-python.org/en/latest/types/mutations/>`__
|
||||
features to add custom mutations to a Django project.
|
||||
|
||||
Simple example
|
||||
--------------
|
||||
|
||||
.. code:: python
|
||||
|
||||
import graphene
|
||||
|
||||
from graphene_django import DjangoObjectType
|
||||
|
||||
from .models import Question
|
||||
|
||||
|
||||
class QuestionType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Question
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class QuestionMutation(graphene.Mutation):
|
||||
class Arguments:
|
||||
# The input arguments for this mutation
|
||||
text = graphene.String(required=True)
|
||||
id = graphene.ID()
|
||||
|
||||
# The class attributes define the response of the mutation
|
||||
question = graphene.Field(QuestionType)
|
||||
|
||||
@classmethod
|
||||
def mutate(cls, root, info, text, id):
|
||||
question = Question.objects.get(pk=id)
|
||||
question.text = text
|
||||
question.save()
|
||||
# Notice we return an instance of this mutation
|
||||
return QuestionMutation(question=question)
|
||||
|
||||
|
||||
class Mutation(graphene.ObjectType):
|
||||
update_question = QuestionMutation.Field()
|
||||
|
||||
|
||||
Django Forms
|
||||
------------
|
||||
|
||||
Graphene-Django comes with mutation classes that will convert the fields on Django forms into inputs on a mutation.
|
||||
|
||||
DjangoFormMutation
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code:: python
|
||||
|
||||
from graphene_django.forms.mutation import DjangoFormMutation
|
||||
|
||||
class MyForm(forms.Form):
|
||||
name = forms.CharField()
|
||||
|
||||
class MyMutation(DjangoFormMutation):
|
||||
class Meta:
|
||||
form_class = MyForm
|
||||
|
||||
``MyMutation`` will automatically receive an ``input`` argument. This argument should be a ``dict`` where the key is ``name`` and the value is a string.
|
||||
|
||||
DjangoModelFormMutation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``DjangoModelFormMutation`` will pull the fields from a ``ModelForm``.
|
||||
|
||||
.. code:: python
|
||||
|
||||
from graphene_django.forms.mutation import DjangoModelFormMutation
|
||||
|
||||
class Pet(models.Model):
|
||||
name = models.CharField()
|
||||
|
||||
class PetForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Pet
|
||||
fields = ('name',)
|
||||
|
||||
# This will get returned when the mutation completes successfully
|
||||
class PetType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Pet
|
||||
fields = '__all__'
|
||||
|
||||
class PetMutation(DjangoModelFormMutation):
|
||||
pet = Field(PetType)
|
||||
|
||||
class Meta:
|
||||
form_class = PetForm
|
||||
|
||||
``PetMutation`` will grab the fields from ``PetForm`` and turn them into inputs. If the form is valid then the mutation
|
||||
will lookup the ``DjangoObjectType`` for the ``Pet`` model and return that under the key ``pet``. Otherwise it will
|
||||
return a list of errors.
|
||||
|
||||
You can change the input name (default is ``input``) and the return field name (default is the model name lowercase).
|
||||
|
||||
.. code:: python
|
||||
|
||||
class PetMutation(DjangoModelFormMutation):
|
||||
class Meta:
|
||||
form_class = PetForm
|
||||
input_field_name = 'data'
|
||||
return_field_name = 'my_pet'
|
||||
|
||||
Form validation
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Form mutations will call ``is_valid()`` on your forms.
|
||||
|
||||
If the form is valid then the class method ``perform_mutate(form, info)`` is called on the mutation. Override this method
|
||||
to change how the form is saved or to return a different Graphene object type.
|
||||
|
||||
If the form is *not* valid then a list of errors will be returned. These errors have two fields: ``field``, a string
|
||||
containing the name of the invalid form field, and ``messages``, a list of strings with the validation messages.
|
||||
|
||||
DjangoFormInputObjectType
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``DjangoFormInputObjectType`` is used in mutations to create input fields by **using django form** to retrieve input data structure from it. This can be helpful in situations where you need to pass data to several django forms in one mutation.
|
||||
|
||||
.. code:: python
|
||||
|
||||
from graphene_django.forms.types import DjangoFormInputObjectType
|
||||
|
||||
|
||||
class PetFormInput(DjangoFormInputObjectType):
|
||||
# any other fields can be placed here as well as
|
||||
# other djangoforminputobjects and intputobjects
|
||||
class Meta:
|
||||
form_class = PetForm
|
||||
object_type = PetType
|
||||
|
||||
class QuestionFormInput(DjangoFormInputObjectType)
|
||||
class Meta:
|
||||
form_class = QuestionForm
|
||||
object_type = QuestionType
|
||||
|
||||
class SeveralFormsInputData(graphene.InputObjectType):
|
||||
pet = PetFormInput(required=True)
|
||||
question = QuestionFormInput(required=True)
|
||||
|
||||
class SomeSophisticatedMutation(graphene.Mutation):
|
||||
class Arguments:
|
||||
data = SeveralFormsInputData(required=True)
|
||||
|
||||
@staticmethod
|
||||
def mutate(_root, _info, data):
|
||||
pet_form_inst = PetForm(data=data.pet)
|
||||
question_form_inst = QuestionForm(data=data.question)
|
||||
|
||||
if pet_form_inst.is_valid():
|
||||
pet_model_instance = pet_form_inst.save(commit=False)
|
||||
|
||||
if question_form_inst.is_valid():
|
||||
question_model_instance = question_form_inst.save(commit=False)
|
||||
|
||||
# ...
|
||||
|
||||
Additional to **InputObjectType** ``Meta`` class attributes:
|
||||
|
||||
* ``form_class`` is required and should be equal to django form class.
|
||||
* ``object_type`` is not required and used to enable convertion of enum values back to original if model object type ``convert_choices_to_enum`` ``Meta`` class attribute is not set to ``False``. Any data field, which have choices in django, with value ``A_1`` (for example) from client will be automatically converted to ``1`` in mutation data.
|
||||
* ``add_id_field_name`` is used to specify `id` field name (not required, by default equal to ``id``)
|
||||
* ``add_id_field_type`` is used to specify `id` field type (not required, default is ``graphene.ID``)
|
||||
|
||||
Django REST Framework
|
||||
---------------------
|
||||
|
||||
You can re-use your Django Rest Framework serializer with Graphene Django mutations.
|
||||
|
||||
You can create a Mutation based on a serializer by using the `SerializerMutation` base class:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from graphene_django.rest_framework.mutation import SerializerMutation
|
||||
|
||||
class MyAwesomeMutation(SerializerMutation):
|
||||
class Meta:
|
||||
serializer_class = MySerializer
|
||||
|
||||
|
||||
Create/Update Operations
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
By default ModelSerializers accept create and update operations. To
|
||||
customize this use the `model_operations` attribute on the ``SerializerMutation`` class.
|
||||
|
||||
The update operation looks up models by the primary key by default. You can
|
||||
customize the look up with the ``lookup_field`` attribute on the ``SerializerMutation`` class.
|
||||
|
||||
.. code:: python
|
||||
|
||||
from graphene_django.rest_framework.mutation import SerializerMutation
|
||||
from .serializers import MyModelSerializer
|
||||
|
||||
|
||||
class AwesomeModelMutation(SerializerMutation):
|
||||
class Meta:
|
||||
serializer_class = MyModelSerializer
|
||||
model_operations = ['create', 'update']
|
||||
lookup_field = 'id'
|
||||
|
||||
Overriding Update Queries
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Use the method ``get_serializer_kwargs`` to override how updates are applied.
|
||||
|
||||
.. code:: python
|
||||
|
||||
from graphene_django.rest_framework.mutation import SerializerMutation
|
||||
from .serializers import MyModelSerializer
|
||||
|
||||
|
||||
class AwesomeModelMutation(SerializerMutation):
|
||||
class Meta:
|
||||
serializer_class = MyModelSerializer
|
||||
|
||||
@classmethod
|
||||
def get_serializer_kwargs(cls, root, info, **input):
|
||||
if 'id' in input:
|
||||
instance = Post.objects.filter(
|
||||
id=input['id'], owner=info.context.user
|
||||
).first()
|
||||
if instance:
|
||||
return {'instance': instance, 'data': input, 'partial': True}
|
||||
|
||||
else:
|
||||
raise http.Http404
|
||||
|
||||
return {'data': input, 'partial': True}
|
||||
|
||||
|
||||
|
||||
Relay
|
||||
-----
|
||||
|
||||
You can use relay with mutations. A Relay mutation must inherit from
|
||||
``ClientIDMutation`` and implement the ``mutate_and_get_payload`` method:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import graphene
|
||||
from graphene import relay
|
||||
from graphene_django import DjangoObjectType
|
||||
from graphql_relay import from_global_id
|
||||
|
||||
from .queries import QuestionType
|
||||
|
||||
|
||||
class QuestionMutation(relay.ClientIDMutation):
|
||||
class Input:
|
||||
text = graphene.String(required=True)
|
||||
id = graphene.ID()
|
||||
|
||||
question = graphene.Field(QuestionType)
|
||||
|
||||
@classmethod
|
||||
def mutate_and_get_payload(cls, root, info, text, id):
|
||||
question = Question.objects.get(pk=from_global_id(id)[1])
|
||||
question.text = text
|
||||
question.save()
|
||||
return QuestionMutation(question=question)
|
||||
|
||||
Notice that the ``class Arguments`` is renamed to ``class Input`` with relay.
|
||||
This is due to a deprecation of ``class Arguments`` in graphene 2.0.
|
||||
|
||||
Relay ClientIDMutation accept a ``clientIDMutation`` argument.
|
||||
This argument is also sent back to the client with the mutation result
|
||||
(you do not have to do anything). For services that manage
|
||||
a pool of many GraphQL requests in bulk, the ``clientIDMutation``
|
||||
allows you to match up a specific mutation with the response.
|
||||
|
||||
|
||||
|
||||
Django Database Transactions
|
||||
----------------------------
|
||||
|
||||
Django gives you a few ways to control how database transactions are managed.
|
||||
|
||||
Tying transactions to HTTP requests
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A common way to handle transactions in Django is to wrap each request in a transaction.
|
||||
Set ``ATOMIC_REQUESTS`` settings to ``True`` in the configuration of each database for
|
||||
which you want to enable this behavior.
|
||||
|
||||
It works like this. Before calling ``GraphQLView`` Django starts a transaction. If the
|
||||
response is produced without problems, Django commits the transaction. If the view, a
|
||||
``DjangoFormMutation`` or a ``DjangoModelFormMutation`` produces an exception, Django
|
||||
rolls back the transaction.
|
||||
|
||||
.. warning::
|
||||
|
||||
While the simplicity of this transaction model is appealing, it also makes it
|
||||
inefficient when traffic increases. Opening a transaction for every request has some
|
||||
overhead. The impact on performance depends on the query patterns of your application
|
||||
and on how well your database handles locking.
|
||||
|
||||
Check the next section for a better solution.
|
||||
|
||||
Tying transactions to mutations
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A mutation can contain multiple fields, just like a query. There's one important
|
||||
distinction between queries and mutations, other than the name:
|
||||
|
||||
..
|
||||
|
||||
`While query fields are executed in parallel, mutation fields run in series, one
|
||||
after the other.`
|
||||
|
||||
This means that if we send two ``incrementCredits`` mutations in one request, the first
|
||||
is guaranteed to finish before the second begins, ensuring that we don't end up with a
|
||||
race condition with ourselves.
|
||||
|
||||
On the other hand, if the first ``incrementCredits`` runs successfully but the second
|
||||
one does not, the operation cannot be retried as it is. That's why is a good idea to
|
||||
run all mutation fields in a transaction, to guarantee all occur or nothing occurs.
|
||||
|
||||
To enable this behavior for all databases set the graphene ``ATOMIC_MUTATIONS`` settings
|
||||
to ``True`` in your settings file:
|
||||
|
||||
.. code:: python
|
||||
|
||||
GRAPHENE = {
|
||||
# ...
|
||||
"ATOMIC_MUTATIONS": True,
|
||||
}
|
||||
|
||||
On the contrary, if you want to enable this behavior for a specific database, set
|
||||
``ATOMIC_MUTATIONS`` to ``True`` in your database settings:
|
||||
|
||||
.. code:: python
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
# ...
|
||||
"ATOMIC_MUTATIONS": True,
|
||||
},
|
||||
# ...
|
||||
}
|
||||
|
||||
Now, given the following example mutation:
|
||||
|
||||
.. code::
|
||||
|
||||
mutation IncreaseCreditsTwice {
|
||||
|
||||
increaseCredits1: increaseCredits(input: { amount: 10 }) {
|
||||
balance
|
||||
errors {
|
||||
field
|
||||
messages
|
||||
}
|
||||
}
|
||||
|
||||
increaseCredits2: increaseCredits(input: { amount: -1 }) {
|
||||
balance
|
||||
errors {
|
||||
field
|
||||
messages
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
The server is going to return something like:
|
||||
|
||||
.. code:: json
|
||||
|
||||
{
|
||||
"data": {
|
||||
"increaseCredits1": {
|
||||
"balance": 10.0,
|
||||
"errors": []
|
||||
},
|
||||
"increaseCredits2": {
|
||||
"balance": null,
|
||||
"errors": [
|
||||
{
|
||||
"field": "amount",
|
||||
"message": "Amount should be a positive number"
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
But the balance will remain the same.
|
476
docs/queries.rst
Normal file
476
docs/queries.rst
Normal file
|
@ -0,0 +1,476 @@
|
|||
.. _queries-objecttypes:
|
||||
|
||||
Queries & ObjectTypes
|
||||
=====================
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
Graphene-Django offers a host of features for performing GraphQL queries.
|
||||
|
||||
Graphene-Django ships with a special ``DjangoObjectType`` that automatically transforms a Django Model
|
||||
into a ``ObjectType`` for you.
|
||||
|
||||
|
||||
Full example
|
||||
~~~~~~~~~~~~
|
||||
|
||||
.. code:: python
|
||||
|
||||
# my_app/schema.py
|
||||
|
||||
import graphene
|
||||
from graphene_django import DjangoObjectType
|
||||
|
||||
from .models import Question
|
||||
|
||||
class QuestionType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Question
|
||||
fields = ("id", "question_text")
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
questions = graphene.List(QuestionType)
|
||||
question_by_id = graphene.Field(QuestionType, id=graphene.String())
|
||||
|
||||
def resolve_questions(root, info, **kwargs):
|
||||
# Querying a list
|
||||
return Question.objects.all()
|
||||
|
||||
def resolve_question_by_id(root, info, id):
|
||||
# Querying a single question
|
||||
return Question.objects.get(pk=id)
|
||||
|
||||
|
||||
Specifying which fields to include
|
||||
----------------------------------
|
||||
|
||||
By default, ``DjangoObjectType`` will present all fields on a Model through GraphQL.
|
||||
If you only want a subset of fields to be present, you can do so using
|
||||
``fields`` or ``exclude``. It is strongly recommended that you explicitly set
|
||||
all fields that should be exposed using the fields attribute.
|
||||
This will make it less likely to result in unintentionally exposing data when
|
||||
your models change.
|
||||
|
||||
Setting neither ``fields`` nor ``exclude`` is deprecated and will raise a warning, you should at least explicitly make
|
||||
``DjangoObjectType`` include all fields in the model as described below.
|
||||
|
||||
``fields``
|
||||
~~~~~~~~~~
|
||||
|
||||
Show **only** these fields on the model:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from graphene_django import DjangoObjectType
|
||||
from .models import Question
|
||||
|
||||
class QuestionType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Question
|
||||
fields = ("id", "question_text")
|
||||
|
||||
You can also set the ``fields`` attribute to the special value ``"__all__"`` to indicate that all fields in the model should be used.
|
||||
|
||||
For example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from graphene_django import DjangoObjectType
|
||||
from .models import Question
|
||||
|
||||
class QuestionType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Question
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
``exclude``
|
||||
~~~~~~~~~~~
|
||||
|
||||
Show all fields **except** those in ``exclude``:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from graphene_django import DjangoObjectType
|
||||
from .models import Question
|
||||
|
||||
class QuestionType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Question
|
||||
exclude = ("question_text",)
|
||||
|
||||
|
||||
Customising fields
|
||||
------------------
|
||||
|
||||
You can completely overwrite a field, or add new fields, to a ``DjangoObjectType`` using a Resolver:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from graphene_django import DjangoObjectType
|
||||
from .models import Question
|
||||
|
||||
class QuestionType(DjangoObjectType):
|
||||
|
||||
class Meta:
|
||||
model = Question
|
||||
fields = ("id", "question_text")
|
||||
|
||||
extra_field = graphene.String()
|
||||
|
||||
def resolve_extra_field(self, info):
|
||||
return "hello!"
|
||||
|
||||
|
||||
Choices to Enum conversion
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
By default Graphene-Django will convert any Django fields that have `choices`_
|
||||
defined into a GraphQL enum type.
|
||||
|
||||
.. _choices: https://docs.djangoproject.com/en/2.2/ref/models/fields/#choices
|
||||
|
||||
For example the following ``Model`` and ``DjangoObjectType``:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from django.db import models
|
||||
from graphene_django import DjangoObjectType
|
||||
|
||||
class PetModel(models.Model):
|
||||
kind = models.CharField(
|
||||
max_length=100,
|
||||
choices=(("cat", "Cat"), ("dog", "Dog"))
|
||||
)
|
||||
|
||||
class Pet(DjangoObjectType):
|
||||
class Meta:
|
||||
model = PetModel
|
||||
fields = ("id", "kind",)
|
||||
|
||||
Results in the following GraphQL schema definition:
|
||||
|
||||
.. code:: graphql
|
||||
|
||||
type Pet {
|
||||
id: ID!
|
||||
kind: PetModelKind!
|
||||
}
|
||||
|
||||
enum PetModelKind {
|
||||
CAT
|
||||
DOG
|
||||
}
|
||||
|
||||
You can disable this automatic conversion by setting
|
||||
``convert_choices_to_enum`` attribute to ``False`` on the ``DjangoObjectType``
|
||||
``Meta`` class.
|
||||
|
||||
.. code:: python
|
||||
|
||||
from graphene_django import DjangoObjectType
|
||||
from .models import PetModel
|
||||
|
||||
class Pet(DjangoObjectType):
|
||||
class Meta:
|
||||
model = PetModel
|
||||
fields = ("id", "kind",)
|
||||
convert_choices_to_enum = False
|
||||
|
||||
.. code:: graphql
|
||||
|
||||
type Pet {
|
||||
id: ID!
|
||||
kind: String!
|
||||
}
|
||||
|
||||
You can also set ``convert_choices_to_enum`` to a list of fields that should be
|
||||
automatically converted into enums:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from graphene_django import DjangoObjectType
|
||||
from .models import PetModel
|
||||
|
||||
class Pet(DjangoObjectType):
|
||||
class Meta:
|
||||
model = PetModel
|
||||
fields = ("id", "kind",)
|
||||
convert_choices_to_enum = ["kind"]
|
||||
|
||||
**Note:** Setting ``convert_choices_to_enum = []`` is the same as setting it to
|
||||
``False``.
|
||||
|
||||
|
||||
Related models
|
||||
--------------
|
||||
|
||||
Say you have the following models:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from django.db import models
|
||||
|
||||
class Category(models.Model):
|
||||
foo = models.CharField(max_length=256)
|
||||
|
||||
class Question(models.Model):
|
||||
category = models.ForeignKey(Category, on_delete=models.CASCADE)
|
||||
|
||||
|
||||
When ``Question`` is published as a ``DjangoObjectType`` and you want to add ``Category`` as a query-able field like so:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from graphene_django import DjangoObjectType
|
||||
from .models import Question
|
||||
|
||||
class QuestionType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Question
|
||||
fields = ("category",)
|
||||
|
||||
Then all query-able related models must be defined as DjangoObjectType subclass,
|
||||
or they will fail to show if you are trying to query those relation fields. You only
|
||||
need to create the most basic class for this to work:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from graphene_django import DjangoObjectType
|
||||
from .models import Category
|
||||
|
||||
class CategoryType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Category
|
||||
fields = ("foo",)
|
||||
|
||||
.. _django-objecttype-get-queryset:
|
||||
|
||||
Default QuerySet
|
||||
-----------------
|
||||
|
||||
If you are using ``DjangoObjectType`` you can define a custom `get_queryset` method.
|
||||
Use this to control filtering on the ObjectType level instead of the Query object level.
|
||||
|
||||
.. code:: python
|
||||
|
||||
from graphene_django.types import DjangoObjectType
|
||||
from .models import Question
|
||||
|
||||
class QuestionType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Question
|
||||
fields = "__all__"
|
||||
|
||||
@classmethod
|
||||
def get_queryset(cls, queryset, info):
|
||||
if info.context.user.is_anonymous:
|
||||
return queryset.filter(published=True)
|
||||
return queryset
|
||||
|
||||
Resolvers
|
||||
---------
|
||||
|
||||
When a GraphQL query is received by the ``Schema`` object, it will map it to a "Resolver" related to it.
|
||||
|
||||
This resolve method should follow this format:
|
||||
|
||||
.. code:: python
|
||||
|
||||
def resolve_foo(parent, info, **kwargs):
|
||||
|
||||
Where "foo" is the name of the field declared in the ``Query`` object.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import graphene
|
||||
from .models import Question
|
||||
from .types import QuestionType
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
foo = graphene.List(QuestionType)
|
||||
|
||||
def resolve_foo(root, info, **kwargs):
|
||||
id = kwargs.get("id")
|
||||
return Question.objects.get(id)
|
||||
|
||||
Arguments
|
||||
~~~~~~~~~
|
||||
|
||||
Additionally, Resolvers will receive **any arguments declared in the field definition**. This allows you to provide input arguments in your GraphQL server and can be useful for custom queries.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import graphene
|
||||
from .models import Question
|
||||
from .types import QuestionType
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
question = graphene.Field(
|
||||
QuestionType,
|
||||
foo=graphene.String(),
|
||||
bar=graphene.Int()
|
||||
)
|
||||
|
||||
def resolve_question(root, info, foo=None, bar=None):
|
||||
# If `foo` or `bar` are declared in the GraphQL query they will be here, else None.
|
||||
return Question.objects.filter(foo=foo, bar=bar).first()
|
||||
|
||||
|
||||
Info
|
||||
~~~~
|
||||
|
||||
The ``info`` argument passed to all resolve methods holds some useful information.
|
||||
For Graphene-Django, the ``info.context`` attribute is the ``HTTPRequest`` object
|
||||
that would be familiar to any Django developer. This gives you the full functionality
|
||||
of Django's ``HTTPRequest`` in your resolve methods, such as checking for authenticated users:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import graphene
|
||||
|
||||
from .models import Question
|
||||
from .types import QuestionType
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
questions = graphene.List(QuestionType)
|
||||
|
||||
def resolve_questions(root, info):
|
||||
# See if a user is authenticated
|
||||
if info.context.user.is_authenticated():
|
||||
return Question.objects.all()
|
||||
else:
|
||||
return Question.objects.none()
|
||||
|
||||
|
||||
DjangoObjectTypes
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
A Resolver that maps to a defined `DjangoObjectType` should only use methods that return a queryset.
|
||||
Queryset methods like `values` will return dictionaries, use `defer` instead.
|
||||
|
||||
|
||||
Plain ObjectTypes
|
||||
-----------------
|
||||
|
||||
With Graphene-Django you are not limited to just Django Models - you can use the standard
|
||||
``ObjectType`` to create custom fields or to provide an abstraction between your internal
|
||||
Django models and your external API.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import graphene
|
||||
from .models import Question
|
||||
|
||||
class MyQuestion(graphene.ObjectType):
|
||||
text = graphene.String()
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
question = graphene.Field(MyQuestion, question_id=graphene.String())
|
||||
|
||||
def resolve_question(root, info, question_id):
|
||||
question = Question.objects.get(pk=question_id)
|
||||
return MyQuestion(
|
||||
text=question.question_text
|
||||
)
|
||||
|
||||
For more information and more examples, please see the `core object type documentation <https://docs.graphene-python.org/en/latest/types/objecttypes/>`__.
|
||||
|
||||
|
||||
Relay
|
||||
-----
|
||||
|
||||
`Relay <http://docs.graphene-python.org/en/latest/relay/>`__ with Graphene-Django gives us some additional features:
|
||||
|
||||
- Pagination and slicing.
|
||||
- An abstract ``id`` value which contains enough info for the server to know its type and its id.
|
||||
|
||||
There is one additional import and a single line of code needed to adopt this:
|
||||
|
||||
Full example
|
||||
~~~~~~~~~~~~
|
||||
See the `Relay documentation <https://docs.graphene-python.org/en/latest/relay/nodes/>`__ on
|
||||
the core graphene pages for more information on customizing the Relay experience.
|
||||
|
||||
.. code:: python
|
||||
|
||||
from graphene import relay
|
||||
from graphene_django import DjangoObjectType
|
||||
from .models import Question
|
||||
|
||||
class QuestionType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Question
|
||||
interfaces = (relay.Node,) # make sure you add this
|
||||
fields = "__all__"
|
||||
|
||||
class QuestionConnection(relay.Connection):
|
||||
class Meta:
|
||||
node = QuestionType
|
||||
|
||||
class Query:
|
||||
questions = relay.ConnectionField(QuestionConnection)
|
||||
|
||||
def resolve_questions(root, info, **kwargs):
|
||||
return Question.objects.all()
|
||||
|
||||
You can now execute queries like:
|
||||
|
||||
|
||||
.. code:: graphql
|
||||
|
||||
{
|
||||
questions (first: 2, after: "YXJyYXljb25uZWN0aW9uOjEwNQ==") {
|
||||
pageInfo {
|
||||
startCursor
|
||||
endCursor
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
}
|
||||
edges {
|
||||
cursor
|
||||
node {
|
||||
id
|
||||
question_text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Which returns:
|
||||
|
||||
.. code:: json
|
||||
|
||||
{
|
||||
"data": {
|
||||
"questions": {
|
||||
"pageInfo": {
|
||||
"startCursor": "YXJyYXljb25uZWN0aW9uOjEwNg==",
|
||||
"endCursor": "YXJyYXljb25uZWN0aW9uOjEwNw==",
|
||||
"hasNextPage": true,
|
||||
"hasPreviousPage": false
|
||||
},
|
||||
"edges": [
|
||||
{
|
||||
"cursor": "YXJyYXljb25uZWN0aW9uOjEwNg==",
|
||||
"node": {
|
||||
"id": "UGxhY2VUeXBlOjEwNw==",
|
||||
"question_text": "How did we get here?"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cursor": "YXJyYXljb25uZWN0aW9uOjEwNw==",
|
||||
"node": {
|
||||
"id": "UGxhY2VUeXBlOjEwOA==",
|
||||
"name": "Where are we?"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Note that relay implements :code:`pagination` capabilities automatically, adding a :code:`pageInfo` element, and including :code:`cursor` on nodes. These elements are included in the above example for illustration.
|
||||
|
||||
To learn more about Pagination in general, take a look at `Pagination <https://graphql.org/learn/pagination/>`__ on the GraphQL community site.
|
|
@ -1,3 +1,5 @@
|
|||
sphinx
|
||||
Sphinx==7.0.0
|
||||
sphinx-autobuild==2021.3.14
|
||||
pygments-graphql-lexer==0.1.0
|
||||
# Docs template
|
||||
https://github.com/graphql-python/graphene-python.org/archive/docs.zip
|
||||
http://graphene-python.org/sphinx_graphene_theme.zip
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
Integration with Django Rest Framework
|
||||
======================================
|
||||
|
||||
You can re-use your Django Rest Framework serializer with
|
||||
graphene django.
|
||||
|
||||
|
||||
Mutation
|
||||
--------
|
||||
|
||||
You can create a Mutation based on a serializer by using the
|
||||
`SerializerMutation` base class:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from graphene_django.rest_framework.mutation import SerializerMutation
|
||||
|
||||
class MyAwesomeMutation(SerializerMutation):
|
||||
class Meta:
|
||||
serializer_class = MySerializer
|
||||
|
57
docs/schema.py
Normal file
57
docs/schema.py
Normal 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
|
50
docs/schema.rst
Normal file
50
docs/schema.rst
Normal file
|
@ -0,0 +1,50 @@
|
|||
Schema
|
||||
======
|
||||
|
||||
The ``graphene.Schema`` object describes your data model and provides a GraphQL server with an associated set of resolve methods that know how to fetch data. The most basic schema you can create looks like this:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import graphene
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
pass
|
||||
|
||||
class Mutation(graphene.ObjectType):
|
||||
pass
|
||||
|
||||
schema = graphene.Schema(query=Query, mutation=Mutation)
|
||||
|
||||
|
||||
This schema doesn't do anything yet, but it is ready to accept new Query or Mutation fields.
|
||||
|
||||
|
||||
Adding to the schema
|
||||
--------------------
|
||||
|
||||
If you have defined a ``Query`` or ``Mutation``, you can register them with the schema:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import graphene
|
||||
|
||||
import my_app.schema.Query
|
||||
import my_app.schema.Mutation
|
||||
|
||||
class Query(
|
||||
my_app.schema.Query, # Add your Query objects here
|
||||
graphene.ObjectType
|
||||
):
|
||||
pass
|
||||
|
||||
class Mutation(
|
||||
my_app.schema.Mutation, # Add your Mutation objects here
|
||||
graphene.ObjectType
|
||||
):
|
||||
pass
|
||||
|
||||
schema = graphene.Schema(query=Query, mutation=Mutation)
|
||||
|
||||
You can add as many mixins to the base ``Query`` and ``Mutation`` objects as you like.
|
||||
|
||||
Read more about Schema on the `core graphene docs <https://docs.graphene-python.org/en/latest/types/schema/>`__
|
291
docs/settings.rst
Normal file
291
docs/settings.rst
Normal file
|
@ -0,0 +1,291 @@
|
|||
Settings
|
||||
========
|
||||
|
||||
Graphene-Django can be customised using settings. This page explains each setting and their defaults.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
Add settings to your Django project by creating a Dictionary with name ``GRAPHENE`` in the project's ``settings.py``:
|
||||
|
||||
.. code:: python
|
||||
|
||||
GRAPHENE = {
|
||||
...
|
||||
}
|
||||
|
||||
|
||||
``SCHEMA``
|
||||
----------
|
||||
|
||||
The location of the top-level ``Schema`` class.
|
||||
|
||||
Default: ``None``
|
||||
|
||||
.. code:: python
|
||||
|
||||
GRAPHENE = {
|
||||
'SCHEMA': 'path.to.schema.schema',
|
||||
}
|
||||
|
||||
|
||||
``SCHEMA_OUTPUT``
|
||||
-----------------
|
||||
|
||||
The name of the file where the GraphQL schema output will go.
|
||||
|
||||
Default: ``schema.json``
|
||||
|
||||
.. code:: python
|
||||
|
||||
GRAPHENE = {
|
||||
'SCHEMA_OUTPUT': 'schema.json',
|
||||
}
|
||||
|
||||
|
||||
``SCHEMA_INDENT``
|
||||
-----------------
|
||||
|
||||
The indentation level of the schema output.
|
||||
|
||||
Default: ``2``
|
||||
|
||||
.. code:: python
|
||||
|
||||
GRAPHENE = {
|
||||
'SCHEMA_INDENT': 2,
|
||||
}
|
||||
|
||||
|
||||
``MIDDLEWARE``
|
||||
--------------
|
||||
|
||||
A tuple of middleware that will be executed for each GraphQL query.
|
||||
|
||||
See the `middleware documentation <https://docs.graphene-python.org/en/latest/execution/middleware/>`__ for more information.
|
||||
|
||||
Default: ``()``
|
||||
|
||||
.. code:: python
|
||||
|
||||
GRAPHENE = {
|
||||
'MIDDLEWARE': (
|
||||
'path.to.my.middleware.class',
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
``RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST``
|
||||
------------------------------------------
|
||||
|
||||
Enforces relay queries to have the ``first`` or ``last`` argument.
|
||||
|
||||
Default: ``False``
|
||||
|
||||
.. code:: python
|
||||
|
||||
GRAPHENE = {
|
||||
'RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST': False,
|
||||
}
|
||||
|
||||
|
||||
``RELAY_CONNECTION_MAX_LIMIT``
|
||||
------------------------------
|
||||
|
||||
The maximum size of objects that can be requested through a relay connection.
|
||||
|
||||
Default: ``100``
|
||||
|
||||
.. code:: python
|
||||
|
||||
GRAPHENE = {
|
||||
'RELAY_CONNECTION_MAX_LIMIT': 100,
|
||||
}
|
||||
|
||||
|
||||
``CAMELCASE_ERRORS``
|
||||
--------------------
|
||||
|
||||
When set to ``True`` field names in the ``errors`` object will be camel case.
|
||||
By default they will be snake case.
|
||||
|
||||
Default: ``False``
|
||||
|
||||
.. code:: python
|
||||
|
||||
GRAPHENE = {
|
||||
'CAMELCASE_ERRORS': False,
|
||||
}
|
||||
|
||||
# result = schema.execute(...)
|
||||
print(result.errors)
|
||||
# [
|
||||
# {
|
||||
# 'field': 'test_field',
|
||||
# 'messages': ['This field is required.'],
|
||||
# }
|
||||
# ]
|
||||
|
||||
.. code:: python
|
||||
|
||||
GRAPHENE = {
|
||||
'CAMELCASE_ERRORS': True,
|
||||
}
|
||||
|
||||
# result = schema.execute(...)
|
||||
print(result.errors)
|
||||
# [
|
||||
# {
|
||||
# 'field': 'testField',
|
||||
# 'messages': ['This field is required.'],
|
||||
# }
|
||||
# ]
|
||||
|
||||
|
||||
``DJANGO_CHOICE_FIELD_ENUM_CONVERT``
|
||||
--------------------------------------
|
||||
|
||||
When set to ``True`` Django choice fields are automatically converted into Enum types.
|
||||
|
||||
Can be disabled globally by setting it to ``False``.
|
||||
|
||||
Default: ``True``
|
||||
|
||||
``DJANGO_CHOICE_FIELD_ENUM_V2_NAMING``
|
||||
--------------------------------------
|
||||
|
||||
Set to ``True`` to use the old naming format for the auto generated Enum types from Django choice fields. The old format looks like this: ``{object_name}_{field_name}``
|
||||
|
||||
Default: ``False``
|
||||
|
||||
|
||||
``DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME``
|
||||
----------------------------------------
|
||||
|
||||
Define the path of a function that takes the Django choice field and returns a string to completely customise the naming for the Enum type.
|
||||
|
||||
If set to a function then the ``DJANGO_CHOICE_FIELD_ENUM_V2_NAMING`` setting is ignored.
|
||||
|
||||
Default: ``None``
|
||||
|
||||
.. code:: python
|
||||
|
||||
# myapp.utils
|
||||
def enum_naming(field):
|
||||
if isinstance(field.model, User):
|
||||
return f"CustomUserEnum{field.name.title()}"
|
||||
return f"CustomEnum{field.name.title()}"
|
||||
|
||||
GRAPHENE = {
|
||||
'DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME': "myapp.utils.enum_naming"
|
||||
}
|
||||
|
||||
|
||||
``SUBSCRIPTION_PATH``
|
||||
---------------------
|
||||
|
||||
Define an alternative URL path where subscription operations should be routed.
|
||||
|
||||
The GraphiQL interface will use this setting to intelligently route subscription operations. This is useful if you have more advanced infrastructure requirements that prevent websockets from being handled at the same path (e.g., a WSGI server listening at ``/graphql`` and an ASGI server listening at ``/ws/graphql``).
|
||||
|
||||
Default: ``None``
|
||||
|
||||
.. code:: python
|
||||
|
||||
GRAPHENE = {
|
||||
'SUBSCRIPTION_PATH': "/ws/graphql"
|
||||
}
|
||||
|
||||
|
||||
``GRAPHIQL_HEADER_EDITOR_ENABLED``
|
||||
----------------------------------
|
||||
|
||||
GraphiQL starting from version 1.0.0 allows setting custom headers in similar fashion to query variables.
|
||||
|
||||
Set to ``False`` if you want to disable GraphiQL headers editor tab for some reason.
|
||||
|
||||
This setting is passed to ``headerEditorEnabled`` GraphiQL options, for details refer to GraphiQLDocs_.
|
||||
|
||||
Default: ``True``
|
||||
|
||||
.. code:: python
|
||||
|
||||
GRAPHENE = {
|
||||
'GRAPHIQL_HEADER_EDITOR_ENABLED': True,
|
||||
}
|
||||
|
||||
|
||||
``TESTING_ENDPOINT``
|
||||
--------------------
|
||||
|
||||
Define the graphql endpoint url used for the `GraphQLTestCase` class.
|
||||
|
||||
Default: ``/graphql``
|
||||
|
||||
.. code:: python
|
||||
|
||||
GRAPHENE = {
|
||||
'TESTING_ENDPOINT': '/customEndpoint'
|
||||
}
|
||||
|
||||
|
||||
``GRAPHIQL_SHOULD_PERSIST_HEADERS``
|
||||
-----------------------------------
|
||||
|
||||
Set to ``True`` if you want to persist GraphiQL headers after refreshing the page.
|
||||
|
||||
This setting is passed to ``shouldPersistHeaders`` GraphiQL options, for details refer to GraphiQLDocs_.
|
||||
|
||||
|
||||
Default: ``False``
|
||||
|
||||
.. code:: python
|
||||
|
||||
GRAPHENE = {
|
||||
'GRAPHIQL_SHOULD_PERSIST_HEADERS': False,
|
||||
}
|
||||
|
||||
|
||||
``GRAPHIQL_INPUT_VALUE_DEPRECATION``
|
||||
------------------------------------
|
||||
|
||||
Set to ``True`` if you want GraphiQL to show any deprecated fields on input object types' docs.
|
||||
|
||||
For example, having this schema:
|
||||
|
||||
.. code:: python
|
||||
|
||||
class MyMutationInputType(graphene.InputObjectType):
|
||||
old_field = graphene.String(deprecation_reason="You should now use 'newField' instead.")
|
||||
new_field = graphene.String()
|
||||
|
||||
class MyMutation(graphene.Mutation):
|
||||
class Arguments:
|
||||
input = types.MyMutationInputType()
|
||||
|
||||
GraphiQL will add a ``Show Deprecated Fields`` button to toggle information display on ``oldField`` and its deprecation
|
||||
reason. Otherwise, you would get neither a button nor any information at all on ``oldField``.
|
||||
|
||||
This setting is passed to ``inputValueDeprecation`` GraphiQL options, for details refer to GraphiQLDocs_.
|
||||
|
||||
Default: ``False``
|
||||
|
||||
.. code:: python
|
||||
|
||||
GRAPHENE = {
|
||||
'GRAPHIQL_INPUT_VALUE_DEPRECATION': False,
|
||||
}
|
||||
|
||||
|
||||
.. _GraphiQLDocs: https://graphiql-test.netlify.app/typedoc/modules/graphiql_react#graphiqlprovider-2
|
||||
|
||||
|
||||
``MAX_VALIDATION_ERRORS``
|
||||
------------------------------------
|
||||
|
||||
In case ``validation_rules`` are provided to ``GraphQLView``, if this is set to a non-negative ``int`` value,
|
||||
``graphql.validation.validate`` will stop validation after this number of errors has been reached.
|
||||
If not set or set to ``None``, the maximum number of errors will follow ``graphql.validation.validate`` default
|
||||
*i.e.* 100.
|
||||
|
||||
Default: ``None``
|
42
docs/subscriptions.rst
Normal file
42
docs/subscriptions.rst
Normal 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, you’ll 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.
|
155
docs/testing.rst
Normal file
155
docs/testing.rst
Normal file
|
@ -0,0 +1,155 @@
|
|||
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
|
||||
|
||||
import json
|
||||
|
||||
from graphene_django.utils.testing import GraphQLTestCase
|
||||
|
||||
class MyFancyTestCase(GraphQLTestCase):
|
||||
def test_some_query(self):
|
||||
response = self.query(
|
||||
'''
|
||||
query {
|
||||
myModel {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
''',
|
||||
operation_name='myModel'
|
||||
)
|
||||
|
||||
content = json.loads(response.content)
|
||||
|
||||
# This validates the status code and if you get errors
|
||||
self.assertResponseNoErrors(response)
|
||||
|
||||
# Add some more asserts if you like
|
||||
...
|
||||
|
||||
def test_query_with_variables(self):
|
||||
response = self.query(
|
||||
'''
|
||||
query myModel($id: Int!){
|
||||
myModel(id: $id) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
''',
|
||||
operation_name='myModel',
|
||||
variables={'id': 1}
|
||||
)
|
||||
|
||||
content = json.loads(response.content)
|
||||
|
||||
# This validates the status code and if you get errors
|
||||
self.assertResponseNoErrors(response)
|
||||
|
||||
# Add some more asserts if you like
|
||||
...
|
||||
|
||||
def test_some_mutation(self):
|
||||
response = self.query(
|
||||
'''
|
||||
mutation myMutation($input: MyMutationInput!) {
|
||||
myMutation(input: $input) {
|
||||
my-model {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
''',
|
||||
operation_name='myMutation',
|
||||
input_data={'my_field': 'foo', 'other_field': 'bar'}
|
||||
)
|
||||
|
||||
# This validates the status code and if you get errors
|
||||
self.assertResponseNoErrors(response)
|
||||
|
||||
# Add some more asserts if you like
|
||||
...
|
||||
|
||||
|
||||
For testing mutations that are executed within a transaction you should subclass `GraphQLTransactionTestCase`
|
||||
|
||||
Usage:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import json
|
||||
|
||||
from graphene_django.utils.testing import GraphQLTransactionTestCase
|
||||
|
||||
class MyFancyTransactionTestCase(GraphQLTransactionTestCase):
|
||||
|
||||
def test_some_mutation_that_executes_within_a_transaction(self):
|
||||
response = self.query(
|
||||
'''
|
||||
mutation myMutation($input: MyMutationInput!) {
|
||||
myMutation(input: $input) {
|
||||
my-model {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
''',
|
||||
operation_name='myMutation',
|
||||
input_data={'my_field': 'foo', 'other_field': 'bar'}
|
||||
)
|
||||
|
||||
# This validates the status code and if you get errors
|
||||
self.assertResponseNoErrors(response)
|
||||
|
||||
# Add some more asserts if you like
|
||||
...
|
||||
|
||||
Using pytest
|
||||
------------
|
||||
|
||||
To use pytest define a simple fixture using the query helper below
|
||||
|
||||
.. code:: python
|
||||
|
||||
# Create a fixture using the graphql_query helper and `client` fixture from `pytest-django`.
|
||||
import json
|
||||
import pytest
|
||||
from graphene_django.utils.testing import graphql_query
|
||||
|
||||
@pytest.fixture
|
||||
def client_query(client):
|
||||
def func(*args, **kwargs):
|
||||
return graphql_query(*args, **kwargs, client=client)
|
||||
|
||||
return func
|
||||
|
||||
# Test you query using the client_query fixture
|
||||
def test_some_query(client_query):
|
||||
response = client_query(
|
||||
'''
|
||||
query {
|
||||
myModel {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
''',
|
||||
operation_name='myModel'
|
||||
)
|
||||
|
||||
content = json.loads(response.content)
|
||||
assert 'errors' not in content
|
|
@ -1,20 +1,13 @@
|
|||
Introduction tutorial - Graphene and Django
|
||||
Basic Tutorial
|
||||
===========================================
|
||||
|
||||
Graphene has a number of additional features that are designed to make
|
||||
working with Django *really simple*.
|
||||
|
||||
Our primary focus here is to give a good understanding of how to connect models from Django ORM to graphene object types.
|
||||
|
||||
A good idea is to check the `graphene <http://docs.graphene-python.org/en/latest/>`__ documentation first.
|
||||
Graphene Django has a number of additional features that are designed to make
|
||||
working with Django easy. Our primary focus in this tutorial is to give a good
|
||||
understanding of how to connect models from Django ORM to Graphene object types.
|
||||
|
||||
Set up the Django project
|
||||
-------------------------
|
||||
|
||||
You can find the entire project in ``examples/cookbook-plain``.
|
||||
|
||||
----
|
||||
|
||||
We will set up the project, create the following:
|
||||
|
||||
- A Django project called ``cookbook``
|
||||
|
@ -31,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...
|
||||
|
@ -57,18 +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')
|
||||
category = models.ForeignKey(
|
||||
Category, related_name="ingredients", on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
@ -77,39 +70,66 @@ Add ingredients as INSTALLED_APPS:
|
|||
|
||||
.. code:: python
|
||||
|
||||
# cookbook/settings.py
|
||||
|
||||
INSTALLED_APPS = [
|
||||
...
|
||||
# Install the ingredients app
|
||||
'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:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
python manage.py makemigrations
|
||||
python manage.py migrate
|
||||
|
||||
|
||||
|
||||
Load some test data
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Now is a good time to load up some test data. The easiest option will be
|
||||
to `download the
|
||||
ingredients.json <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:
|
||||
|
||||
.. code:: python
|
||||
|
||||
# cookbook/ingredients/admin.py
|
||||
from django.contrib import admin
|
||||
from cookbook.ingredients.models import Category, Ingredient
|
||||
|
||||
admin.site.register(Category)
|
||||
admin.site.register(Ingredient)
|
||||
|
||||
|
||||
Hello GraphQL - Schema and Object Types
|
||||
---------------------------------------
|
||||
|
@ -127,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(graphene.AbstractType):
|
||||
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 marked as 'abstract'. This is
|
||||
because we will now create a project-level query which will combine all
|
||||
our app-level queries.
|
||||
|
||||
Create the parent project-level ``cookbook/schema.py``:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import graphene
|
||||
|
||||
import 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
|
||||
-------------------------
|
||||
|
@ -205,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,
|
||||
|
@ -234,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))),
|
||||
]
|
||||
|
||||
|
||||
|
@ -250,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))),
|
||||
]
|
||||
|
||||
|
||||
|
@ -272,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.
|
||||
|
||||
|
@ -318,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 {
|
||||
|
@ -345,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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -400,125 +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(graphene.AbstractType):
|
||||
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 = kargs.get('id')
|
||||
name = kargs.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 = kargs.get('id')
|
||||
name = kargs.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 so that you are familiar with it as well.
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
Graphene and Django Tutorial using Relay
|
||||
.. _Relay tutorial:
|
||||
|
||||
Relay tutorial
|
||||
========================================
|
||||
|
||||
Graphene has a number of additional features that are designed to make
|
||||
working with Django *really simple*.
|
||||
|
||||
Note: The code in this quickstart is pulled from the `cookbook example
|
||||
app <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/graphql-relay-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:
|
||||
|
@ -118,7 +120,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following:
|
|||
.. code:: python
|
||||
|
||||
# cookbook/ingredients/schema.py
|
||||
from graphene import relay, ObjectType, AbstractType
|
||||
from graphene import relay, ObjectType
|
||||
from graphene_django import DjangoObjectType
|
||||
from graphene_django.filter import DjangoFilterConnectionField
|
||||
|
||||
|
@ -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(AbstractType):
|
||||
class Query(ObjectType):
|
||||
category = relay.Node.Field(CategoryNode)
|
||||
all_categories = DjangoFilterConnectionField(CategoryNode)
|
||||
|
||||
|
@ -158,7 +162,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following:
|
|||
The filtering functionality is provided by
|
||||
`django-filter <https://django-filter.readthedocs.org>`__. See the
|
||||
`usage
|
||||
documentation <https://django-filter.readthedocs.org/en/latest/usage.html#the-filter>`__
|
||||
documentation <https://django-filter.readthedocs.org/en/latest/guide/usage.html#the-filter>`__
|
||||
for details on the format for ``filter_fields``. While optional, this
|
||||
tutorial makes use of this functionality so you will need to install
|
||||
``django-filter`` for this tutorial to work:
|
||||
|
@ -244,7 +248,7 @@ aforementioned GraphiQL we specify that on the params with ``graphiql=True``.
|
|||
|
||||
urlpatterns = [
|
||||
url(r'^admin/', admin.site.urls),
|
||||
url(r'^graphql', GraphQLView.as_view(graphiql=True)),
|
||||
url(r'^graphql$', GraphQLView.as_view(graphiql=True)),
|
||||
]
|
||||
|
||||
|
||||
|
@ -262,7 +266,7 @@ as explained above, we can do so here using:
|
|||
|
||||
urlpatterns = [
|
||||
url(r'^admin/', admin.site.urls),
|
||||
url(r'^graphql', GraphQLView.as_view(graphiql=True, schema=schema)),
|
||||
url(r'^graphql$', GraphQLView.as_view(graphiql=True, schema=schema)),
|
||||
]
|
||||
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
@ -345,3 +349,10 @@ Or you can get only 'meat' ingredients containing the letter 'e':
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Final Steps
|
||||
^^^^^^^^^^^
|
||||
|
||||
We have created a GraphQL endpoint that will work with Relay, but for Relay to work it needs access to a (non python) schema. Instructions to export the schema can be found on the `Introspection Schema <http://docs.graphene-python.org/projects/django/en/latest/introspection/>`__ part of this guide.
|
||||
|
|
29
docs/validation.rst
Normal file
29
docs/validation.rst
Normal 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
0
examples/__init__.py
Normal file
|
@ -3,7 +3,7 @@ Cookbook Example Django Project
|
|||
|
||||
This example project demos integration between Graphene and Django.
|
||||
The project contains two apps, one named `ingredients` and another
|
||||
named `recepies`.
|
||||
named `recipes`.
|
||||
|
||||
Getting started
|
||||
---------------
|
||||
|
@ -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
|
||||
|
@ -60,5 +60,14 @@ 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#testing-our-graphql-schema)
|
||||
(See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial-plain/#testing-our-graphql-schema)
|
||||
for some example queries)
|
||||
|
||||
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
|
||||
```
|
||||
|
|
0
examples/cookbook-plain/__init__.py
Normal file
0
examples/cookbook-plain/__init__.py
Normal 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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
# Generated by Django 2.0 on 2018-10-18 17:46
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("ingredients", "0002_auto_20161104_0050"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="category",
|
||||
options={"verbose_name_plural": "Categories"},
|
||||
),
|
||||
]
|
|
@ -2,6 +2,9 @@ from django.db import models
|
|||
|
||||
|
||||
class Category(models.Model):
|
||||
class Meta:
|
||||
verbose_name_plural = "Categories"
|
||||
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
def __str__(self):
|
||||
|
@ -11,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')
|
||||
category = models.ForeignKey(
|
||||
Category, related_name="ingredients", on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
|
|
@ -1,41 +1,38 @@
|
|||
import graphene
|
||||
from graphene_django.types import DjangoObjectType
|
||||
|
||||
from cookbook.ingredients.models import Category, Ingredient
|
||||
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(graphene.AbstractType):
|
||||
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, args, context, info):
|
||||
def resolve_all_categories(self, context):
|
||||
return Category.objects.all()
|
||||
|
||||
def resolve_all_ingredients(self, args, context, info):
|
||||
def resolve_all_ingredients(self, context):
|
||||
# We can easily optimize query count in the resolve method
|
||||
return Ingredient.objects.select_related('category').all()
|
||||
|
||||
def resolve_category(self, args, context, info):
|
||||
id = args.get('id')
|
||||
name = args.get('name')
|
||||
return Ingredient.objects.select_related("category").all()
|
||||
|
||||
def resolve_category(self, context, id=None, name=None):
|
||||
if id is not None:
|
||||
return Category.objects.get(pk=id)
|
||||
|
||||
|
@ -44,10 +41,7 @@ class Query(graphene.AbstractType):
|
|||
|
||||
return None
|
||||
|
||||
def resolve_ingredient(self, args, context, info):
|
||||
id = args.get('id')
|
||||
name = args.get('name')
|
||||
|
||||
def resolve_ingredient(self, context, id=None, name=None):
|
||||
if id is not None:
|
||||
return Ingredient.objects.get(pk=id)
|
||||
|
||||
|
|
1
examples/cookbook-plain/cookbook/ingredients/tests.py
Normal file
1
examples/cookbook-plain/cookbook/ingredients/tests.py
Normal file
|
@ -0,0 +1 @@
|
|||
# Create your tests here.
|
1
examples/cookbook-plain/cookbook/ingredients/views.py
Normal file
1
examples/cookbook-plain/cookbook/ingredients/views.py
Normal file
|
@ -0,0 +1 @@
|
|||
# Create your views here.
|
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 2.0 on 2018-10-18 17:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("recipes", "0002_auto_20161104_0106"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="recipeingredient",
|
||||
name="unit",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("unit", "Units"),
|
||||
("kg", "Kilograms"),
|
||||
("l", "Litres"),
|
||||
("st", "Shots"),
|
||||
],
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -1,20 +1,28 @@
|
|||
from django.db import models
|
||||
|
||||
from cookbook.ingredients.models import Ingredient
|
||||
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')
|
||||
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"),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -1,33 +1,29 @@
|
|||
import graphene
|
||||
from graphene_django.types import DjangoObjectType
|
||||
|
||||
from cookbook.recipes.models import Recipe, RecipeIngredient
|
||||
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(graphene.AbstractType):
|
||||
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, args, context, info):
|
||||
id = args.get('id')
|
||||
title = args.get('title')
|
||||
|
||||
def resolve_recipe(self, context, id=None, title=None):
|
||||
if id is not None:
|
||||
return Recipe.objects.get(pk=id)
|
||||
|
||||
|
@ -36,17 +32,15 @@ class Query(graphene.AbstractType):
|
|||
|
||||
return None
|
||||
|
||||
def resolve_recipeingredient(self, args, context, info):
|
||||
id = args.get('id')
|
||||
|
||||
def resolve_recipeingredient(self, context, id=None):
|
||||
if id is not None:
|
||||
return RecipeIngredient.objects.get(pk=id)
|
||||
|
||||
return None
|
||||
|
||||
def resolve_all_recipes(self, args, context, info):
|
||||
def resolve_all_recipes(self, context):
|
||||
return Recipe.objects.all()
|
||||
|
||||
def resolve_all_recipeingredients(self, args, context, info):
|
||||
related = ['recipe', 'ingredient']
|
||||
def resolve_all_recipeingredients(self, context):
|
||||
related = ["recipe", "ingredient"]
|
||||
return RecipeIngredient.objects.select_related(*related).all()
|
||||
|
|
1
examples/cookbook-plain/cookbook/recipes/tests.py
Normal file
1
examples/cookbook-plain/cookbook/recipes/tests.py
Normal file
|
@ -0,0 +1 @@
|
|||
# Create your tests here.
|
1
examples/cookbook-plain/cookbook/recipes/views.py
Normal file
1
examples/cookbook-plain/cookbook/recipes/views.py
Normal file
|
@ -0,0 +1 @@
|
|||
# Create your views here.
|
|
@ -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)
|
||||
|
|
|
@ -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_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',
|
||||
'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,11 +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/'
|
||||
|
||||
GRAPHENE = {
|
||||
'SCHEMA': 'cookbook.schema.schema',
|
||||
'SCHEMA_INDENT': 2,
|
||||
}
|
||||
STATIC_URL = "/static/"
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
from django.conf.urls import url
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
|
||||
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/", GraphQLView.as_view(graphiql=True)),
|
||||
]
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
django~=3.2
|
||||
graphene
|
||||
graphene-django
|
||||
graphql-core
|
||||
django==1.9
|
||||
graphene-django>=3.1
|
||||
|
|
|
@ -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#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)
|
||||
|
|
0
examples/cookbook/__init__.py
Normal file
0
examples/cookbook/__init__.py
Normal file
|
@ -2,9 +2,11 @@ from django.contrib import admin
|
|||
|
||||
from cookbook.ingredients.models import Category, Ingredient
|
||||
|
||||
|
||||
@admin.register(Ingredient)
|
||||
class IngredientAdmin(admin.ModelAdmin):
|
||||
list_display = ("id","name","category")
|
||||
list_editable = ("name","category")
|
||||
|
||||
list_display = ("id", "name", "category")
|
||||
list_editable = ("name", "category")
|
||||
|
||||
|
||||
admin.site.register(Category)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -10,8 +10,10 @@ 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')
|
||||
notes = models.TextField(null=True, blank=True)
|
||||
category = models.ForeignKey(
|
||||
Category, related_name="ingredients", on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
|
|
@ -1,34 +1,35 @@
|
|||
from cookbook.ingredients.models import Category, Ingredient
|
||||
from graphene import AbstractType, Node
|
||||
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(AbstractType):
|
||||
class Query:
|
||||
category = Node.Field(CategoryNode)
|
||||
all_categories = DjangoFilterConnectionField(CategoryNode)
|
||||
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
|
||||
# Create your tests here.
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
|
||||
# Create your views here.
|
||||
|
|
|
@ -2,9 +2,11 @@ from django.contrib import admin
|
|||
|
||||
from cookbook.recipes.models import Recipe, RecipeIngredient
|
||||
|
||||
|
||||
class RecipeIngredientInline(admin.TabularInline):
|
||||
model = RecipeIngredient
|
||||
model = RecipeIngredient
|
||||
|
||||
|
||||
@admin.register(Recipe)
|
||||
class RecipeAdmin(admin.ModelAdmin):
|
||||
inlines = [RecipeIngredientInline]
|
||||
inlines = [RecipeIngredientInline]
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -6,15 +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"),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -1,30 +1,32 @@
|
|||
from cookbook.recipes.models import Recipe, RecipeIngredient
|
||||
from graphene import AbstractType, Node
|
||||
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(AbstractType):
|
||||
class Query:
|
||||
recipe = Node.Field(RecipeNode)
|
||||
all_recipes = DjangoFilterConnectionField(RecipeNode)
|
||||
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
|
||||
# Create your tests here.
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
|
||||
# Create your views here.
|
||||
|
|
|
@ -1,12 +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.recipes.schema.Query, cookbook.ingredients.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)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# flake8: noqa
|
||||
"""
|
||||
Django settings for cookbook project.
|
||||
|
||||
|
@ -20,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
|
||||
|
@ -31,64 +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',
|
||||
'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"),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,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
|
||||
|
||||
|
@ -129,9 +122,4 @@ USE_TZ = True
|
|||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.9/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
GRAPHENE = {
|
||||
'SCHEMA': 'cookbook.schema.schema',
|
||||
'SCHEMA_INDENT': 2,
|
||||
}
|
||||
STATIC_URL = "/static/"
|
||||
|
|
|
@ -4,6 +4,6 @@ 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)),
|
||||
]
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
graphene
|
||||
graphene-django
|
||||
graphql-core
|
||||
django==1.9
|
||||
django-filter==0.11.0
|
||||
graphene>=2.1,<3
|
||||
graphene-django>=2.1,<3
|
||||
graphql-core>=2.1,<3
|
||||
django==3.2.25
|
||||
django-filter>=2
|
||||
|
|
2
examples/cookbook/setup.cfg
Normal file
2
examples/cookbook/setup.cfg
Normal file
|
@ -0,0 +1,2 @@
|
|||
[flake8]
|
||||
exclude=migrations,.git,__pycache__
|
32
examples/django_test_settings.py
Normal file
32
examples/django_test_settings.py
Normal 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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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', 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
|
||||
|
@ -13,7 +17,7 @@ class Character(models.Model):
|
|||
|
||||
class Faction(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
hero = models.ForeignKey(Character)
|
||||
hero = models.ForeignKey(Character, on_delete=models.CASCADE)
|
||||
|
||||
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, related_name='ships')
|
||||
faction = models.ForeignKey(Faction, on_delete=models.CASCADE, related_name="ships")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user