├── .coveragerc ├── .gitignore ├── LICENSE ├── README.md ├── pyproject.toml ├── setup.cfg ├── tests.sh ├── tests ├── __init__.py ├── conftest.py ├── graphql │ ├── __init__.py │ ├── children.graphql │ ├── children.json │ ├── document_1.graphql │ ├── document_1.json │ ├── documents_all.graphql │ ├── documents_all.json │ ├── image_1.graphql │ ├── image_1.json │ ├── image_1_rendition.graphql │ ├── image_1_rendition.json │ ├── image_2.graphql │ ├── image_2.json │ ├── images_all.graphql │ ├── images_all.json │ ├── menus_all.graphql │ ├── menus_all.json │ ├── prefetch.graphql │ ├── prefetch.json │ ├── settings.graphql │ ├── settings.json │ ├── settings_fail.graphql │ ├── settings_fail.json │ ├── settings_relay.graphql │ ├── settings_relay.json │ ├── showmenus.graphql │ ├── showmenus.json │ ├── site.graphql │ ├── site.json │ ├── snippets_1.graphql │ ├── snippets_1.json │ ├── snippets_1_relay.graphql │ ├── snippets_1_relay.json │ ├── test_app_1_get_home.graphql │ ├── test_app_1_get_home.json │ ├── test_app_1_get_home_latest.graphql │ ├── test_app_1_get_home_latest.json │ ├── test_app_1_get_home_latest_relay.graphql │ ├── test_app_1_get_home_latest_relay.json │ ├── test_app_1_get_home_none.graphql │ ├── test_app_1_get_home_none.json │ ├── test_app_1_get_home_relay.graphql │ ├── test_app_1_get_home_relay.json │ ├── test_app_1_get_home_revision.graphql │ ├── test_app_1_get_home_revision.json │ ├── test_app_1_get_home_revision_fail.graphql │ ├── test_app_1_get_home_revision_fail.json │ ├── test_app_1_get_home_revision_relay.graphql │ ├── test_app_1_get_home_revision_relay.json │ ├── test_app_1_get_pages.graphql │ ├── test_app_1_get_pages.json │ ├── test_app_1_get_pages_parent.graphql │ ├── test_app_1_get_pages_parent.json │ ├── test_app_1_get_pages_parent_fail.graphql │ ├── test_app_1_get_pages_parent_fail.json │ ├── test_app_1_get_pages_relay.graphql │ ├── test_app_1_get_pages_relay.json │ ├── test_app_2_streamfield.graphql │ ├── test_app_2_streamfield.json │ ├── test_app_2_streamfield_relay.graphql │ ├── test_app_2_streamfield_relay.json │ ├── test_user_admin.graphql │ ├── test_user_admin.json │ ├── test_user_anonymous.graphql │ └── test_user_anonymous.json ├── test_auth.py ├── test_basic_pages.py ├── test_documents.py ├── test_forms.py ├── test_images.py ├── test_menus.py ├── test_permissions.py ├── test_project │ ├── .gitignore │ ├── db.sqlite3 │ ├── manage.py │ ├── project │ │ ├── __init__.py │ │ ├── settings │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── relay.py │ │ │ └── test.py │ │ ├── urls.py │ │ └── wsgi.py │ ├── test_app_1 │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_create_homepage.py │ │ │ └── __init__.py │ │ ├── models.py │ │ └── templates │ │ │ └── home │ │ │ └── home_page.html │ └── test_app_2 │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── models.py │ │ ├── tests.py │ │ └── views.py ├── test_registry.py ├── test_settings.py ├── test_site.py ├── test_snippets.py ├── test_streamfield.py └── test_wagtail_graphql.py └── wagtail_graphql ├── __init__.py ├── actions.py ├── apps.py ├── permissions.py ├── registry.py ├── relay.py ├── schema.py ├── settings.py └── types ├── __init__.py ├── auth.py ├── converters.py ├── core.py ├── documents.py ├── forms.py ├── images.py ├── menus.py ├── settings.py ├── snippets.py └── streamfield.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | include = wagtail_graphql/* 4 | omit = 5 | 6 | [html] 7 | directory = cover 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg 2 | *.egg-info 3 | *.pyc 4 | *$py.class 5 | *~ 6 | .coverage 7 | coverage.xml 8 | build/ 9 | dist/ 10 | .tox/ 11 | env*/ 12 | tmp/ 13 | .venv* 14 | .cache 15 | .eggs 16 | cover 17 | .pytest* 18 | .mypy_cache 19 | .idea 20 | poetry.lock 21 | htmlcov/ 22 | pip-wheel-metadata 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Tiago Requeijo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |      2 |
3 | 4 | # wagtail-graphql 5 | > An app to automatically add GraphQL support to a Wagtail website 6 | 7 | This [Wagtail](https://wagtail.io/) app adds [GraphQL](https://graphql.org/) types to other Wagtail apps. The objective is for this library to interact with an existing website in a generic way and with minimal effort. 8 | In particular, it makes minimal assumptions about the structure of the website 9 | to allow for a generic API. 10 | 11 | ## Installing / Getting started 12 | 13 | To install as a general app: 14 | 15 | ```shell 16 | pip install wagtail-graphql 17 | ``` 18 | 19 | Add it together with [graphene_django](https://github.com/graphql-python/graphene-django) to the Django INSTALLED_APPS: 20 | 21 | ```python 22 | INSTALLED_APPS = [ 23 | ... 24 | 'wagtail_graphql', 25 | 'graphene_django', 26 | ... 27 | ] 28 | 29 | ``` 30 | 31 | ### Initial Configuration 32 | 33 | Add the required [graphene](https://github.com/graphql-python/graphene) schema `GRAPHENE` and a `GRAPHQL_API` dictionary. 34 | Include all the Wagtail apps the library should generate bindings to in the `APPS` list and optionally specify the prefix for each app in `PREFIX`. To remove a leading part of all the urls for a specific site, specify the `URL_PREFIX` parameter for each needed host. 35 | 36 | ```python 37 | GRAPHENE = { 38 | 'SCHEMA': 'wagtail_graphql.schema.schema', 39 | } 40 | 41 | GRAPHQL_API = { 42 | 'APPS': [ 43 | 'home' 44 | ], 45 | 'PREFIX': { 46 | 'home': '' # optional, prefix for all the app classes generated by the wrapper 47 | }, 48 | 'URL_PREFIX': { 49 | 'localhost': '/home' # optional, read from the site information if not specified 50 | } 51 | } 52 | ``` 53 | The example above generates bindings for the `home` app, . Every url in this example 54 | will be stripped of the initial `/home` substring. 55 | 56 | Finally, set up the GraphQL views in the project `urls.py`. 57 | For example, to add two endpoints for GraphQL and the [GraphiQL](https://github.com/graphql/graphiql) IDE: 58 | 59 | ```python 60 | from django.views.decorators.csrf import csrf_exempt 61 | from graphene_django.views import GraphQLView 62 | 63 | urlpatterns = [ 64 | ... 65 | url(r'^api/graphql', csrf_exempt(GraphQLView.as_view())), 66 | url(r'^api/graphiql', csrf_exempt(GraphQLView.as_view(graphiql=True, pretty=True)), 67 | ... 68 | ] 69 | ``` 70 | Note that the urls above need to appear before the `wagtail_urls` catchall entry. 71 | 72 | #### Images 73 | 74 | To be able to generate urls for images the following also needs to be included in the project's `urls.py`: 75 | 76 | ```python 77 | from wagtail.images.views.serve import ServeView 78 | 79 | urlpatterns = [ 80 | ... 81 | url(r'^images/([^/]*)/(\d*)/([^/]*)/[^/]*$', ServeView.as_view(), name='wagtailimages_serve'), 82 | ... 83 | ] 84 | ``` 85 | 86 | 87 | ### Multi-site configuration 88 | This library works transparently with a multi-site Wagtail install without any extra configuration required. To strip a custom leading prefix for each site, specify each host in the `URL_PREFIX`. For exaple, for two hosts `host1.example.com` and `host2.example.com`: 89 | 90 | ``` 91 | GRAPHQL_API = { 92 | ... 93 | 'URL_PREFIX': { 94 | 'host1.example.com': '/prefix1', 95 | 'host2.example.com': '/prefix2' 96 | } 97 | ... 98 | } 99 | ``` 100 | Note that the prefix for a site is taken from the root page url if a host is not included in the `URL_PREFIX` dictionary. 101 | 102 | 103 | ## Developing 104 | 105 | To develop this library, download the source code and install a local version in your Wagtail website. 106 | 107 | 108 | ## Features 109 | 110 | This project is intended to require minimal configuration and interaction. It currently supports 111 | 112 | * [Page models](https://docs.wagtail.io/en/master/topics/pages.html) 113 | * [Snippets](https://docs.wagtail.io/en/master/topics/snippets.html) 114 | * Images 115 | * Documents 116 | * [StreamFields](https://docs.wagtail.io/en/master/topics/streamfield.html) with [Basic Blocks](https://docs.wagtail.io/en/naster/topics/streamfield.html#basic-block-types) and [StructBlocks](https://docs.wagtail.io/en/master/topics/streamfield.html#structblock) 117 | 118 | 119 | ## Contributing 120 | 121 | If you'd like to contribute, please fork the repository and use a feature 122 | branch. Pull requests are welcome. 123 | 124 | ## Links 125 | 126 | - Repository: https://github.com/tr11/wagtail-graphql 127 | - Issue tracker: https://github.com/tr11/wagtail-graphql/issues 128 | 129 | ## Licensing 130 | 131 | The code in this project is licensed under MIT license. 132 | 133 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "wagtail-graphql" 3 | version = "0.2.1" 4 | description = "An app to automatically add GraphQL support to a Wagtail website" 5 | license = "MIT" 6 | readme = "README.md" 7 | authors = [ 8 | "Tiago Requeijo " 9 | ] 10 | repository = "https://github.com/tr11/wagtail-graphql" 11 | homepage = "https://github.com/tr11/wagtail-graphql" 12 | keywords = ['wagtail', 'graphql', 'api', 'wagtail-graphql'] 13 | 14 | [tool.poetry.dependencies] 15 | python = "~3.6||~3.7" 16 | graphene_django = "^2.2" 17 | graphene_django_optimizer = "^0.3.5" 18 | wagtail = "^2.3" 19 | wagtailmenus = {version = "^2.12", optional = true} 20 | python-dateutil = "^2.6" 21 | 22 | [tool.poetry.extras] 23 | menus = ["wagtailmenus"] 24 | 25 | [tool.poetry.dev-dependencies] 26 | pytest = "^4.0" 27 | pytest-django = "^3.4" 28 | pytest-cov = "^2.6" 29 | flake8 = "^3.6" 30 | pytest-pythonpath = "^0.7.3" 31 | mypy = "^0.670.0" 32 | pytest-flake8 = "^1.0" 33 | pytest-mypy = "^0.3.2" 34 | django-cors-headers = "^2.4" 35 | 36 | [build-system] 37 | requires = ["poetry>=0.12"] 38 | build-backend = "poetry.masonry.api" 39 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | addopts = --cov=wagtail_graphql --cov-append --cov-report term-missing --flake8 --mypy 3 | norecursedirs = 4 | tests/test_project 5 | 6 | DJANGO_SETTINGS_MODULE = tests.test_project.project.settings.test 7 | python_paths = ./tests/test_project 8 | django_find_project = false 9 | filterwarnings = 10 | ignore::DeprecationWarning 11 | flake8-max-line-length = 120 12 | flake8-max-complexity = 10 13 | 14 | [aliases] 15 | test=pytest 16 | 17 | [mypy] 18 | ignore_missing_imports = True 19 | 20 | [flake8] 21 | max-line-length = 120 22 | exclude = 23 | .git 24 | .venv 25 | __pycache__ 26 | htmlcov 27 | 28 | max-complexity = 10 -------------------------------------------------------------------------------- /tests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | coverage erase 3 | pytest --ds tests.test_project.project.settings.test 4 | pytest --cov-report=html --ds tests.test_project.project.settings.relay 5 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tr11/wagtail-graphql/1c10172f2fb11a5568a8099de00ed2afb59340d7/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture(scope='session') 5 | def django_db_setup(): 6 | """Avoid creating/setting up the test database""" 7 | pass 8 | 9 | 10 | @pytest.fixture 11 | def db_access_without_rollback_and_truncate(request, django_db_setup, django_db_blocker): 12 | django_db_blocker.unblock() 13 | request.addfinalizer(django_db_blocker.restore) 14 | -------------------------------------------------------------------------------- /tests/graphql/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os.path 3 | 4 | 5 | def get_query(*name): 6 | name = '_'.join(name) 7 | folder = os.path.dirname(__file__) 8 | query = open(os.path.join(folder, name + '.graphql'), 'rt').read() 9 | result = json.load(open(os.path.join(folder, name + '.json'), 'rt')) 10 | return query, result 11 | 12 | 13 | def assert_query(client, *name): 14 | query, result = get_query(*name) 15 | response = client.post('/graphql', {"query": query}) 16 | assert response.status_code == 200 17 | response_json = response.json() 18 | assert 'errors' not in response_json 19 | print(result) 20 | print(response_json) 21 | assert result == response_json 22 | 23 | 24 | def assert_query_fail(client, *name): 25 | query, result = get_query(*name) 26 | response = client.post('/graphql', {"query": query}) 27 | assert response.status_code == 200 28 | response_json = response.json() 29 | assert 'errors' in response_json 30 | print(result) 31 | print(response_json) 32 | assert result == response_json 33 | -------------------------------------------------------------------------------- /tests/graphql/children.graphql: -------------------------------------------------------------------------------- 1 | { 2 | page(url:"/") { 3 | children { 4 | id 5 | title 6 | urlPath 7 | path 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /tests/graphql/children.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "page": { 4 | "children": [ 5 | { 6 | "id": 4, 7 | "path": "000100010001", 8 | "title": "TEST", 9 | "urlPath": "/test" 10 | }, 11 | { 12 | "id": 5, 13 | "path": "000100010002", 14 | "title": "Blank Page", 15 | "urlPath": "/blank-page" 16 | }, 17 | { 18 | "id": 6, 19 | "path": "000100010003", 20 | "title": "Form", 21 | "urlPath": "/form" 22 | } 23 | ] 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /tests/graphql/document_1.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | document(id: 1) { 3 | id 4 | title 5 | file 6 | createdAt 7 | uploadedByUser { 8 | username 9 | } 10 | fileSize 11 | fileHash 12 | tags 13 | url 14 | filename 15 | fileExtension 16 | } 17 | } -------------------------------------------------------------------------------- /tests/graphql/document_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "document": { 4 | "createdAt": "2019-01-06T00:18:43.593597+00:00", 5 | "file": "documents/README.md", 6 | "fileExtension": "md", 7 | "fileHash": "31d1a4311d0097df396dbe4dae9524eb8cb58d0f", 8 | "fileSize": 3768, 9 | "filename": "README.md", 10 | "id": "1", 11 | "tags": [ 12 | "code", 13 | "readme" 14 | ], 15 | "title": "README.md", 16 | "uploadedByUser": { 17 | "username": "admin" 18 | }, 19 | "url": "/documents/1/README.md" 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /tests/graphql/documents_all.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | documents { 3 | id 4 | title 5 | file 6 | createdAt 7 | uploadedByUser { 8 | username 9 | } 10 | fileSize 11 | fileHash 12 | tags 13 | url 14 | filename 15 | fileExtension 16 | } 17 | } -------------------------------------------------------------------------------- /tests/graphql/documents_all.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "documents": [ 4 | { 5 | "createdAt": "2019-01-06T00:18:43.593597+00:00", 6 | "file": "documents/README.md", 7 | "fileExtension": "md", 8 | "fileHash": "31d1a4311d0097df396dbe4dae9524eb8cb58d0f", 9 | "fileSize": 3768, 10 | "filename": "README.md", 11 | "id": "1", 12 | "tags": [ 13 | "code", 14 | "readme" 15 | ], 16 | "title": "README.md", 17 | "uploadedByUser": { 18 | "username": "admin" 19 | }, 20 | "url": "/documents/1/README.md" 21 | }, 22 | { 23 | "createdAt": "2019-01-06T00:24:08.697420+00:00", 24 | "file": "documents/LICENSE", 25 | "fileExtension": "", 26 | "fileHash": "f9f9ca2b33321473e39cfc9b29b0965c758b9e81", 27 | "fileSize": 1071, 28 | "filename": "LICENSE", 29 | "id": "2", 30 | "tags": [ 31 | "license", 32 | "code" 33 | ], 34 | "title": "LICENSE", 35 | "uploadedByUser": { 36 | "username": "admin" 37 | }, 38 | "url": "/documents/2/LICENSE" 39 | } 40 | ] 41 | } 42 | } -------------------------------------------------------------------------------- /tests/graphql/image_1.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | image(id: 1) { 3 | id 4 | title 5 | file 6 | width 7 | height 8 | createdAt 9 | uploadedByUser { 10 | id 11 | username 12 | } 13 | fileSize 14 | fileHash 15 | tags 16 | hasFocalPoint 17 | focalPoint { 18 | left 19 | top 20 | right 21 | bottom 22 | x 23 | y 24 | height 25 | width 26 | } 27 | url 28 | urlLink 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /tests/graphql/image_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "image": { 4 | "createdAt": "2019-01-05T20:40:58.197215+00:00", 5 | "file": "original_images/download.png", 6 | "fileHash": "52f99ff31278d071d8c55f55fb387bc618c35571", 7 | "fileSize": 2185, 8 | "focalPoint": null, 9 | "hasFocalPoint": false, 10 | "height": 73, 11 | "id": "1", 12 | "tags": [ 13 | "tag1", 14 | "tag2" 15 | ], 16 | "title": "wagtail logo", 17 | "uploadedByUser": { 18 | "id": "1", 19 | "username": "admin" 20 | }, 21 | "url": "/images/KAMAET37GqDroKyRgjd5yY4CsvE=/1/original/", 22 | "urlLink": "/media/images/download.original.png", 23 | "width": 200 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /tests/graphql/image_1_rendition.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | image(id: 2) { 3 | id 4 | title 5 | file 6 | width 7 | height 8 | createdAt 9 | uploadedByUser { 10 | id 11 | username 12 | } 13 | fileSize 14 | fileHash 15 | tags 16 | hasFocalPoint 17 | focalPoint { 18 | left 19 | top 20 | right 21 | bottom 22 | x 23 | y 24 | height 25 | width 26 | } 27 | url(rendition: "fill-300x150|jpegquality-60") 28 | urlLink(rendition: "fill-300x150|jpegquality-60") 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /tests/graphql/image_1_rendition.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "image": { 4 | "createdAt": "2019-01-05T20:44:46.356363+00:00", 5 | "file": "original_images/download_hxHClaK.png", 6 | "fileHash": "52f99ff31278d071d8c55f55fb387bc618c35571", 7 | "fileSize": 2185, 8 | "focalPoint": { 9 | "bottom": 72, 10 | "height": 73, 11 | "left": 0, 12 | "right": 66, 13 | "top": 0, 14 | "width": 66, 15 | "x": 33, 16 | "y": 36 17 | }, 18 | "hasFocalPoint": true, 19 | "height": 73, 20 | "id": "2", 21 | "tags": [ 22 | "tag4", 23 | "tag3" 24 | ], 25 | "title": "wagtail logo with focal point", 26 | "uploadedByUser": { 27 | "id": "1", 28 | "username": "admin" 29 | }, 30 | "url": "/images/IBct65AiLwRo1nSc6jrGC-YdeTg=/2/fill-300x150%7Cjpegquality-60/", 31 | "urlLink": "/media/images/download_hxHClaK.51fa8394.fill-300x150.jpegquality-60.png", 32 | "width": 200 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /tests/graphql/image_2.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | image(id: 2) { 3 | id 4 | title 5 | file 6 | width 7 | height 8 | createdAt 9 | uploadedByUser { 10 | id 11 | username 12 | } 13 | fileSize 14 | fileHash 15 | tags 16 | hasFocalPoint 17 | focalPoint { 18 | left 19 | top 20 | right 21 | bottom 22 | x 23 | y 24 | height 25 | width 26 | } 27 | url 28 | urlLink 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /tests/graphql/image_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "image": { 4 | "createdAt": "2019-01-05T20:44:46.356363+00:00", 5 | "file": "original_images/download_hxHClaK.png", 6 | "fileHash": "52f99ff31278d071d8c55f55fb387bc618c35571", 7 | "fileSize": 2185, 8 | "focalPoint": { 9 | "bottom": 72, 10 | "height": 73, 11 | "left": 0, 12 | "right": 66, 13 | "top": 0, 14 | "width": 66, 15 | "x": 33, 16 | "y": 36 17 | }, 18 | "hasFocalPoint": true, 19 | "height": 73, 20 | "id": "2", 21 | "tags": [ 22 | "tag4", 23 | "tag3" 24 | ], 25 | "title": "wagtail logo with focal point", 26 | "uploadedByUser": { 27 | "id": "1", 28 | "username": "admin" 29 | }, 30 | "url": "/images/JKHgyjh9SMorj6s8XUE6ths3on0=/2/fill-66x73-c100/", 31 | "urlLink": "/media/images/download_hxHClaK.51fa8394.fill-66x73-c100.png", 32 | "width": 200 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /tests/graphql/images_all.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | images { 3 | id 4 | title 5 | file 6 | width 7 | height 8 | createdAt 9 | uploadedByUser { 10 | id 11 | username 12 | } 13 | fileSize 14 | fileHash 15 | tags 16 | hasFocalPoint 17 | focalPoint { 18 | left 19 | top 20 | right 21 | bottom 22 | x 23 | y 24 | height 25 | width 26 | } 27 | url 28 | urlLink 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /tests/graphql/images_all.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "images": [ 4 | { 5 | "createdAt": "2019-01-05T20:40:58.197215+00:00", 6 | "file": "original_images/download.png", 7 | "fileHash": "52f99ff31278d071d8c55f55fb387bc618c35571", 8 | "fileSize": 2185, 9 | "focalPoint": null, 10 | "hasFocalPoint": false, 11 | "height": 73, 12 | "id": "1", 13 | "tags": [ 14 | "tag1", 15 | "tag2" 16 | ], 17 | "title": "wagtail logo", 18 | "uploadedByUser": { 19 | "id": "1", 20 | "username": "admin" 21 | }, 22 | "url": "/images/KAMAET37GqDroKyRgjd5yY4CsvE=/1/original/", 23 | "urlLink": "/media/images/download.original.png", 24 | "width": 200 25 | }, 26 | { 27 | "createdAt": "2019-01-05T20:44:46.356363+00:00", 28 | "file": "original_images/download_hxHClaK.png", 29 | "fileHash": "52f99ff31278d071d8c55f55fb387bc618c35571", 30 | "fileSize": 2185, 31 | "focalPoint": { 32 | "bottom": 72, 33 | "height": 73, 34 | "left": 0, 35 | "right": 66, 36 | "top": 0, 37 | "width": 66, 38 | "x": 33, 39 | "y": 36 40 | }, 41 | "hasFocalPoint": true, 42 | "height": 73, 43 | "id": "2", 44 | "tags": [ 45 | "tag4", 46 | "tag3" 47 | ], 48 | "title": "wagtail logo with focal point", 49 | "uploadedByUser": { 50 | "id": "1", 51 | "username": "admin" 52 | }, 53 | "url": "/images/JKHgyjh9SMorj6s8XUE6ths3on0=/2/fill-66x73-c100/", 54 | "urlLink": "/media/images/download_hxHClaK.51fa8394.fill-66x73-c100.png", 55 | "width": 200 56 | } 57 | ] 58 | } 59 | } -------------------------------------------------------------------------------- /tests/graphql/menus_all.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | mainMenu { 3 | maxLevels 4 | menuItems { 5 | id 6 | sortOrder 7 | linkPage { 8 | urlPath 9 | } 10 | linkUrl 11 | urlAppend 12 | handle 13 | linkText 14 | allowSubnav 15 | } 16 | } 17 | secondaryMenus { 18 | title 19 | handle 20 | heading 21 | maxLevels 22 | menuItems { 23 | id 24 | sortOrder 25 | linkPage { 26 | urlPath 27 | } 28 | linkUrl 29 | urlAppend 30 | handle 31 | linkText 32 | allowSubnav 33 | } 34 | } 35 | secondaryMenu(handle: "menu1handle") { 36 | title 37 | handle 38 | heading 39 | maxLevels 40 | menuItems { 41 | id 42 | sortOrder 43 | linkPage { 44 | urlPath 45 | } 46 | linkUrl 47 | urlAppend 48 | handle 49 | linkText 50 | allowSubnav 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/graphql/menus_all.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "mainMenu": [ 4 | { 5 | "maxLevels": "A_2", 6 | "menuItems": [ 7 | { 8 | "allowSubnav": true, 9 | "handle": "", 10 | "id": "1", 11 | "linkPage": { 12 | "urlPath": "" 13 | }, 14 | "linkText": "", 15 | "linkUrl": null, 16 | "sortOrder": 0, 17 | "urlAppend": "" 18 | }, 19 | { 20 | "allowSubnav": true, 21 | "handle": "", 22 | "id": "2", 23 | "linkPage": { 24 | "urlPath": "/test" 25 | }, 26 | "linkText": "", 27 | "linkUrl": null, 28 | "sortOrder": 1, 29 | "urlAppend": "" 30 | } 31 | ] 32 | } 33 | ], 34 | "secondaryMenu": { 35 | "handle": "menu1handle", 36 | "heading": "", 37 | "maxLevels": "A_1", 38 | "menuItems": [ 39 | { 40 | "allowSubnav": true, 41 | "handle": "", 42 | "id": "1", 43 | "linkPage": { 44 | "urlPath": "" 45 | }, 46 | "linkText": "", 47 | "linkUrl": null, 48 | "sortOrder": 0, 49 | "urlAppend": "" 50 | } 51 | ], 52 | "title": "menu1" 53 | }, 54 | "secondaryMenus": [ 55 | { 56 | "handle": "menu1handle", 57 | "heading": "", 58 | "maxLevels": "A_1", 59 | "menuItems": [ 60 | { 61 | "allowSubnav": true, 62 | "handle": "", 63 | "id": "1", 64 | "linkPage": { 65 | "urlPath": "" 66 | }, 67 | "linkText": "", 68 | "linkUrl": null, 69 | "sortOrder": 0, 70 | "urlAppend": "" 71 | } 72 | ], 73 | "title": "menu1" 74 | } 75 | ] 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/graphql/prefetch.graphql: -------------------------------------------------------------------------------- 1 | { 2 | pages { 3 | contentType 4 | } 5 | } -------------------------------------------------------------------------------- /tests/graphql/prefetch.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "pages": [ 4 | { 5 | "contentType": "test_app_1.HomePage" 6 | }, 7 | { 8 | "contentType": "test_app_2.PageTypeA" 9 | }, 10 | { 11 | "contentType": "test_app_1.HomePage" 12 | }, 13 | { 14 | "contentType": "test_app_1.FormPage" 15 | } 16 | ] 17 | } 18 | } -------------------------------------------------------------------------------- /tests/graphql/settings.graphql: -------------------------------------------------------------------------------- 1 | { 2 | settings(name: "Test_app_2SiteBranding") { 3 | ...on Test_app_2SiteBranding { 4 | id 5 | siteSetting1 6 | siteSetting2 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /tests/graphql/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "settings": { 4 | "id": "1", 5 | "siteSetting1": "this is setting 1", 6 | "siteSetting2": "this is setting 2" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /tests/graphql/settings_fail.graphql: -------------------------------------------------------------------------------- 1 | { 2 | settings(name: "Test_app_2SiteBrandingMMMM") { 3 | ...on Test_app_2SiteBranding { 4 | id 5 | siteSetting1 6 | siteSetting2 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /tests/graphql/settings_fail.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "settings": null 4 | }, 5 | "errors": [ 6 | { 7 | "locations": [ 8 | { 9 | "column": 3, 10 | "line": 2 11 | } 12 | ], 13 | "message": "Settings 'Test_app_2SiteBrandingMMMM' not found.", 14 | "path": [ 15 | "settings" 16 | ] 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /tests/graphql/settings_relay.graphql: -------------------------------------------------------------------------------- 1 | { 2 | settings(name: "Test_app_2SiteBranding") { 3 | ...on Test_app_2SiteBranding { 4 | id 5 | siteSetting1 6 | siteSetting2 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /tests/graphql/settings_relay.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "settings": { 4 | "id": "VGVzdF9hcHBfMlNpdGVCcmFuZGluZzox", 5 | "siteSetting1": "this is setting 1", 6 | "siteSetting2": "this is setting 2" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /tests/graphql/showmenus.graphql: -------------------------------------------------------------------------------- 1 | { 2 | showInMenus { 3 | path 4 | id 5 | urlPath 6 | } 7 | } -------------------------------------------------------------------------------- /tests/graphql/showmenus.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "showInMenus": [ 4 | { 5 | "id": 3, 6 | "path": "00010001", 7 | "urlPath": "" 8 | } 9 | ] 10 | } 11 | } -------------------------------------------------------------------------------- /tests/graphql/site.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | root { 3 | id 4 | hostname 5 | port 6 | siteName 7 | rootPage { 8 | title 9 | } 10 | isDefaultSite 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/graphql/site.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "root": { 4 | "hostname": "localhost", 5 | "id": "2", 6 | "isDefaultSite": true, 7 | "port": 80, 8 | "rootPage": { 9 | "title": "Home" 10 | }, 11 | "siteName": "THIS is the NAME" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /tests/graphql/snippets_1.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | snippets(typename: "Test_app_1Advert") { 3 | __typename 4 | ... on Test_app_1Advert { 5 | id 6 | url 7 | text 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /tests/graphql/snippets_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "snippets": [ 4 | { 5 | "__typename": "Test_app_1Advert", 6 | "id": "1", 7 | "text": "TEST", 8 | "url": "http://www.google.com" 9 | }, 10 | { 11 | "__typename": "Test_app_1Advert", 12 | "id": "2", 13 | "text": "ANOTHER", 14 | "url": "http://www.google.com/something" 15 | } 16 | ] 17 | } 18 | } -------------------------------------------------------------------------------- /tests/graphql/snippets_1_relay.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | snippets(typename: "Test_app_1Advert") { 3 | __typename 4 | ... on Test_app_1Advert { 5 | id 6 | url 7 | text 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /tests/graphql/snippets_1_relay.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "snippets": [ 4 | { 5 | "__typename": "Test_app_1Advert", 6 | "id": "VGVzdF9hcHBfMUFkdmVydDox", 7 | "text": "TEST", 8 | "url": "http://www.google.com" 9 | }, 10 | { 11 | "__typename": "Test_app_1Advert", 12 | "id": "VGVzdF9hcHBfMUFkdmVydDoy", 13 | "text": "ANOTHER", 14 | "url": "http://www.google.com/something" 15 | } 16 | ] 17 | } 18 | } -------------------------------------------------------------------------------- /tests/graphql/test_app_1_get_home.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | page(id: 3) { 3 | __typename 4 | id 5 | title 6 | urlPath 7 | contentType 8 | slug 9 | path 10 | depth 11 | seoTitle 12 | numchild 13 | revision 14 | firstPublishedAt 15 | lastPublishedAt 16 | latestRevisionCreatedAt 17 | live 18 | goLiveAt 19 | expireAt 20 | expired 21 | locked 22 | draftTitle 23 | hasUnpublishedChanges 24 | ... on Test_app_1HomePage { 25 | fieldChar 26 | fieldInt 27 | fieldBool 28 | fieldDate 29 | fieldDatetime 30 | fieldUrl 31 | fieldDecimal 32 | fieldEmail 33 | fieldFloat 34 | fieldDuration 35 | fieldIntp 36 | fieldSmallintp 37 | fieldSmallint 38 | fieldText 39 | fieldTime 40 | fieldIp 41 | fieldUuid 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /tests/graphql/test_app_1_get_home.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "page": { 4 | "__typename": "Test_app_1HomePage", 5 | "contentType": "test_app_1.HomePage", 6 | "depth": 2, 7 | "draftTitle": "Home", 8 | "expireAt": null, 9 | "expired": false, 10 | "fieldBool": true, 11 | "fieldChar": "ABC", 12 | "fieldDate": "2019-01-04", 13 | "fieldDatetime": "2019-01-04T16:00:00+00:00", 14 | "fieldDecimal": "100.22", 15 | "fieldDuration": "0:10:23", 16 | "fieldEmail": "user@user.com", 17 | "fieldFloat": 34324.324234, 18 | "fieldInt": 100, 19 | "fieldIntp": 11, 20 | "fieldIp": "127.0.0.2", 21 | "fieldSmallint": -3645, 22 | "fieldSmallintp": 31, 23 | "fieldText": "fkjsdhfsdkjhfsdkfsdkjfhk", 24 | "fieldTime": "10:00:00", 25 | "fieldUrl": "https://www.google.com", 26 | "fieldUuid": "2ac46971-a928-4550-9b8a-7456971867ed", 27 | "firstPublishedAt": "2019-01-04T23:53:21.623211+00:00", 28 | "goLiveAt": null, 29 | "hasUnpublishedChanges": false, 30 | "id": 3, 31 | "lastPublishedAt": "2019-02-24T01:29:09.746194+00:00", 32 | "latestRevisionCreatedAt": "2019-02-24T01:29:09.670168+00:00", 33 | "live": true, 34 | "locked": false, 35 | "numchild": 3, 36 | "path": "00010001", 37 | "revision": null, 38 | "seoTitle": null, 39 | "slug": "home", 40 | "title": "Home", 41 | "urlPath": "" 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /tests/graphql/test_app_1_get_home_latest.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | page(url: "/", revision:-1) { 3 | __typename 4 | id 5 | title 6 | urlPath 7 | contentType 8 | slug 9 | path 10 | depth 11 | seoTitle 12 | numchild 13 | revision 14 | firstPublishedAt 15 | lastPublishedAt 16 | latestRevisionCreatedAt 17 | live 18 | goLiveAt 19 | expireAt 20 | expired 21 | locked 22 | draftTitle 23 | hasUnpublishedChanges 24 | ... on Test_app_1HomePage { 25 | fieldChar 26 | fieldInt 27 | fieldBool 28 | fieldDate 29 | fieldDatetime 30 | fieldUrl 31 | fieldDecimal 32 | fieldEmail 33 | fieldFloat 34 | fieldDuration 35 | fieldIntp 36 | fieldSmallintp 37 | fieldSmallint 38 | fieldText 39 | fieldTime 40 | fieldIp 41 | fieldUuid 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /tests/graphql/test_app_1_get_home_latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "page": { 4 | "__typename": "Test_app_1HomePage", 5 | "contentType": "test_app_1.HomePage", 6 | "depth": 2, 7 | "draftTitle": "Home", 8 | "expireAt": null, 9 | "expired": false, 10 | "fieldBool": true, 11 | "fieldChar": "ABC", 12 | "fieldDate": "2019-01-04", 13 | "fieldDatetime": "2019-01-04T16:00:00+00:00", 14 | "fieldDecimal": "100.22", 15 | "fieldDuration": "0:10:23", 16 | "fieldEmail": "user@user.com", 17 | "fieldFloat": 34324.324234, 18 | "fieldInt": 100, 19 | "fieldIntp": 11, 20 | "fieldIp": "127.0.0.2", 21 | "fieldSmallint": -3645, 22 | "fieldSmallintp": 31, 23 | "fieldText": "fkjsdhfsdkjhfsdkfsdkjfhk", 24 | "fieldTime": "10:00:00", 25 | "fieldUrl": "https://www.google.com", 26 | "fieldUuid": "2ac46971-a928-4550-9b8a-7456971867ed", 27 | "firstPublishedAt": "2019-01-04T23:53:21.623211+00:00", 28 | "goLiveAt": null, 29 | "hasUnpublishedChanges": false, 30 | "id": 3, 31 | "lastPublishedAt": "2019-01-05T18:54:33.736000+00:00", 32 | "latestRevisionCreatedAt": "2019-02-24T01:29:09.670168+00:00", 33 | "live": true, 34 | "locked": false, 35 | "numchild": 3, 36 | "path": "00010001", 37 | "revision": 12, 38 | "seoTitle": null, 39 | "slug": "home", 40 | "title": "Home", 41 | "urlPath": "" 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /tests/graphql/test_app_1_get_home_latest_relay.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | page(url: "/", revision:-1) { 3 | __typename 4 | id 5 | title 6 | urlPath 7 | contentType 8 | slug 9 | path 10 | depth 11 | seoTitle 12 | numchild 13 | revision 14 | firstPublishedAt 15 | lastPublishedAt 16 | latestRevisionCreatedAt 17 | live 18 | goLiveAt 19 | expireAt 20 | expired 21 | locked 22 | draftTitle 23 | hasUnpublishedChanges 24 | ... on Test_app_1HomePage { 25 | fieldChar 26 | fieldInt 27 | fieldBool 28 | fieldDate 29 | fieldDatetime 30 | fieldUrl 31 | fieldDecimal 32 | fieldEmail 33 | fieldFloat 34 | fieldDuration 35 | fieldIntp 36 | fieldSmallintp 37 | fieldSmallint 38 | fieldText 39 | fieldTime 40 | fieldIp 41 | fieldUuid 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /tests/graphql/test_app_1_get_home_latest_relay.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "page": { 4 | "__typename": "Test_app_1HomePage", 5 | "contentType": "test_app_1.HomePage", 6 | "depth": 2, 7 | "draftTitle": "Home", 8 | "expireAt": null, 9 | "expired": false, 10 | "fieldBool": true, 11 | "fieldChar": "ABC", 12 | "fieldDate": "2019-01-04", 13 | "fieldDatetime": "2019-01-04T16:00:00+00:00", 14 | "fieldDecimal": "100.22", 15 | "fieldDuration": "0:10:23", 16 | "fieldEmail": "user@user.com", 17 | "fieldFloat": 34324.324234, 18 | "fieldInt": 100, 19 | "fieldIntp": 11, 20 | "fieldIp": "127.0.0.2", 21 | "fieldSmallint": -3645, 22 | "fieldSmallintp": 31, 23 | "fieldText": "fkjsdhfsdkjhfsdkfsdkjfhk", 24 | "fieldTime": "10:00:00", 25 | "fieldUrl": "https://www.google.com", 26 | "fieldUuid": "2ac46971-a928-4550-9b8a-7456971867ed", 27 | "firstPublishedAt": "2019-01-04T23:53:21.623211+00:00", 28 | "goLiveAt": null, 29 | "hasUnpublishedChanges": false, 30 | "id": "VGVzdF9hcHBfMUhvbWVQYWdlOjM=", 31 | "lastPublishedAt": "2019-01-05T18:54:33.736000+00:00", 32 | "latestRevisionCreatedAt": "2019-02-24T01:29:09.670168+00:00", 33 | "live": true, 34 | "locked": false, 35 | "numchild": 3, 36 | "path": "00010001", 37 | "revision": 12, 38 | "seoTitle": null, 39 | "slug": "home", 40 | "title": "Home", 41 | "urlPath": "" 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /tests/graphql/test_app_1_get_home_none.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | page(id: 103, revision:-1) { 3 | __typename 4 | id 5 | title 6 | urlPath 7 | contentType 8 | slug 9 | path 10 | depth 11 | seoTitle 12 | numchild 13 | revision 14 | firstPublishedAt 15 | lastPublishedAt 16 | latestRevisionCreatedAt 17 | live 18 | goLiveAt 19 | expireAt 20 | expired 21 | locked 22 | draftTitle 23 | hasUnpublishedChanges 24 | ... on Test_app_1HomePage { 25 | fieldChar 26 | fieldInt 27 | fieldBool 28 | fieldDate 29 | fieldDatetime 30 | fieldUrl 31 | fieldDecimal 32 | fieldEmail 33 | fieldFloat 34 | fieldDuration 35 | fieldIntp 36 | fieldSmallintp 37 | fieldSmallint 38 | fieldText 39 | fieldTime 40 | fieldIp 41 | fieldUuid 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /tests/graphql/test_app_1_get_home_none.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "page": null 4 | } 5 | } -------------------------------------------------------------------------------- /tests/graphql/test_app_1_get_home_relay.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | page(id: 3) { 3 | __typename 4 | id 5 | title 6 | urlPath 7 | contentType 8 | slug 9 | path 10 | depth 11 | seoTitle 12 | numchild 13 | revision 14 | firstPublishedAt 15 | lastPublishedAt 16 | latestRevisionCreatedAt 17 | live 18 | goLiveAt 19 | expireAt 20 | expired 21 | locked 22 | draftTitle 23 | hasUnpublishedChanges 24 | ... on Test_app_1HomePage { 25 | fieldChar 26 | fieldInt 27 | fieldBool 28 | fieldDate 29 | fieldDatetime 30 | fieldUrl 31 | fieldDecimal 32 | fieldEmail 33 | fieldFloat 34 | fieldDuration 35 | fieldIntp 36 | fieldSmallintp 37 | fieldSmallint 38 | fieldText 39 | fieldTime 40 | fieldIp 41 | fieldUuid 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /tests/graphql/test_app_1_get_home_relay.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "page": { 4 | "__typename": "Test_app_1HomePage", 5 | "contentType": "test_app_1.HomePage", 6 | "depth": 2, 7 | "draftTitle": "Home", 8 | "expireAt": null, 9 | "expired": false, 10 | "fieldBool": true, 11 | "fieldChar": "ABC", 12 | "fieldDate": "2019-01-04", 13 | "fieldDatetime": "2019-01-04T16:00:00+00:00", 14 | "fieldDecimal": "100.22", 15 | "fieldDuration": "0:10:23", 16 | "fieldEmail": "user@user.com", 17 | "fieldFloat": 34324.324234, 18 | "fieldInt": 100, 19 | "fieldIntp": 11, 20 | "fieldIp": "127.0.0.2", 21 | "fieldSmallint": -3645, 22 | "fieldSmallintp": 31, 23 | "fieldText": "fkjsdhfsdkjhfsdkfsdkjfhk", 24 | "fieldTime": "10:00:00", 25 | "fieldUrl": "https://www.google.com", 26 | "fieldUuid": "2ac46971-a928-4550-9b8a-7456971867ed", 27 | "firstPublishedAt": "2019-01-04T23:53:21.623211+00:00", 28 | "goLiveAt": null, 29 | "hasUnpublishedChanges": false, 30 | "id": "VGVzdF9hcHBfMUhvbWVQYWdlOjM=", 31 | "lastPublishedAt": "2019-02-24T01:29:09.746194+00:00", 32 | "latestRevisionCreatedAt": "2019-02-24T01:29:09.670168+00:00", 33 | "live": true, 34 | "locked": false, 35 | "numchild": 3, 36 | "path": "00010001", 37 | "revision": null, 38 | "seoTitle": null, 39 | "slug": "home", 40 | "title": "Home", 41 | "urlPath": "" 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /tests/graphql/test_app_1_get_home_revision.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | page(id: 3, revision: 1) { 3 | __typename 4 | id 5 | title 6 | urlPath 7 | contentType 8 | slug 9 | path 10 | depth 11 | seoTitle 12 | numchild 13 | revision 14 | firstPublishedAt 15 | lastPublishedAt 16 | latestRevisionCreatedAt 17 | live 18 | goLiveAt 19 | expireAt 20 | expired 21 | locked 22 | draftTitle 23 | hasUnpublishedChanges 24 | ... on Test_app_1HomePage { 25 | fieldChar 26 | fieldInt 27 | fieldBool 28 | fieldDate 29 | fieldDatetime 30 | fieldUrl 31 | fieldDecimal 32 | fieldEmail 33 | fieldFloat 34 | fieldDuration 35 | fieldIntp 36 | fieldSmallintp 37 | fieldSmallint 38 | fieldText 39 | fieldTime 40 | fieldIp 41 | fieldUuid 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /tests/graphql/test_app_1_get_home_revision.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "page": { 4 | "__typename": "Test_app_1HomePage", 5 | "contentType": "test_app_1.HomePage", 6 | "depth": 2, 7 | "draftTitle": "Home", 8 | "expireAt": null, 9 | "expired": false, 10 | "fieldBool": true, 11 | "fieldChar": "ABC", 12 | "fieldDate": "2019-01-04", 13 | "fieldDatetime": "2019-01-04T16:00:00+00:00", 14 | "fieldDecimal": null, 15 | "fieldDuration": null, 16 | "fieldEmail": null, 17 | "fieldFloat": null, 18 | "fieldInt": 100, 19 | "fieldIntp": null, 20 | "fieldIp": null, 21 | "fieldSmallint": null, 22 | "fieldSmallintp": null, 23 | "fieldText": null, 24 | "fieldTime": null, 25 | "fieldUrl": null, 26 | "fieldUuid": null, 27 | "firstPublishedAt": "2019-01-04T23:53:21.623211+00:00", 28 | "goLiveAt": null, 29 | "hasUnpublishedChanges": false, 30 | "id": 3, 31 | "lastPublishedAt": null, 32 | "latestRevisionCreatedAt": "2019-02-24T01:29:09.670168+00:00", 33 | "live": true, 34 | "locked": false, 35 | "numchild": 3, 36 | "path": "00010001", 37 | "revision": 1, 38 | "seoTitle": null, 39 | "slug": "home", 40 | "title": "Home", 41 | "urlPath": "" 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /tests/graphql/test_app_1_get_home_revision_fail.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | page(id: 3, revision: 0) { 3 | __typename 4 | id 5 | title 6 | urlPath 7 | contentType 8 | slug 9 | path 10 | depth 11 | seoTitle 12 | numchild 13 | revision 14 | firstPublishedAt 15 | lastPublishedAt 16 | latestRevisionCreatedAt 17 | live 18 | goLiveAt 19 | expireAt 20 | expired 21 | locked 22 | draftTitle 23 | hasUnpublishedChanges 24 | ... on Test_app_1HomePage { 25 | fieldChar 26 | fieldInt 27 | fieldBool 28 | fieldDate 29 | fieldDatetime 30 | fieldUrl 31 | fieldDecimal 32 | fieldEmail 33 | fieldFloat 34 | fieldDuration 35 | fieldIntp 36 | fieldSmallintp 37 | fieldSmallint 38 | fieldText 39 | fieldTime 40 | fieldIp 41 | fieldUuid 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /tests/graphql/test_app_1_get_home_revision_fail.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "page": null 4 | }, 5 | "errors": [ 6 | { 7 | "locations": [ 8 | { 9 | "column": 3, 10 | "line": 2 11 | } 12 | ], 13 | "message": "Revision 0 doesn't exist", 14 | "path": [ 15 | "page" 16 | ] 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /tests/graphql/test_app_1_get_home_revision_relay.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | page(id: 3, revision: 1) { 3 | __typename 4 | id 5 | title 6 | urlPath 7 | contentType 8 | slug 9 | path 10 | depth 11 | seoTitle 12 | numchild 13 | revision 14 | firstPublishedAt 15 | lastPublishedAt 16 | latestRevisionCreatedAt 17 | live 18 | goLiveAt 19 | expireAt 20 | expired 21 | locked 22 | draftTitle 23 | hasUnpublishedChanges 24 | ... on Test_app_1HomePage { 25 | fieldChar 26 | fieldInt 27 | fieldBool 28 | fieldDate 29 | fieldDatetime 30 | fieldUrl 31 | fieldDecimal 32 | fieldEmail 33 | fieldFloat 34 | fieldDuration 35 | fieldIntp 36 | fieldSmallintp 37 | fieldSmallint 38 | fieldText 39 | fieldTime 40 | fieldIp 41 | fieldUuid 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /tests/graphql/test_app_1_get_home_revision_relay.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "page": { 4 | "__typename": "Test_app_1HomePage", 5 | "contentType": "test_app_1.HomePage", 6 | "depth": 2, 7 | "draftTitle": "Home", 8 | "expireAt": null, 9 | "expired": false, 10 | "fieldBool": true, 11 | "fieldChar": "ABC", 12 | "fieldDate": "2019-01-04", 13 | "fieldDatetime": "2019-01-04T16:00:00+00:00", 14 | "fieldDecimal": null, 15 | "fieldDuration": null, 16 | "fieldEmail": null, 17 | "fieldFloat": null, 18 | "fieldInt": 100, 19 | "fieldIntp": null, 20 | "fieldIp": null, 21 | "fieldSmallint": null, 22 | "fieldSmallintp": null, 23 | "fieldText": null, 24 | "fieldTime": null, 25 | "fieldUrl": null, 26 | "fieldUuid": null, 27 | "firstPublishedAt": "2019-01-04T23:53:21.623211+00:00", 28 | "goLiveAt": null, 29 | "hasUnpublishedChanges": false, 30 | "id": "VGVzdF9hcHBfMUhvbWVQYWdlOjM=", 31 | "lastPublishedAt": null, 32 | "latestRevisionCreatedAt": "2019-02-24T01:29:09.670168+00:00", 33 | "live": true, 34 | "locked": false, 35 | "numchild": 3, 36 | "path": "00010001", 37 | "revision": 1, 38 | "seoTitle": null, 39 | "slug": "home", 40 | "title": "Home", 41 | "urlPath": "" 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /tests/graphql/test_app_1_get_pages.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | pages { 3 | title 4 | } 5 | } -------------------------------------------------------------------------------- /tests/graphql/test_app_1_get_pages.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "pages": [ 4 | { 5 | "title": "Home" 6 | }, 7 | { 8 | "title": "TEST" 9 | }, 10 | { 11 | "title": "Blank Page" 12 | }, 13 | { 14 | "title": "Form" 15 | } 16 | ] 17 | } 18 | } -------------------------------------------------------------------------------- /tests/graphql/test_app_1_get_pages_parent.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | pages(parent: 3) { 3 | title 4 | } 5 | } -------------------------------------------------------------------------------- /tests/graphql/test_app_1_get_pages_parent.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "pages": [ 4 | { 5 | "title": "TEST" 6 | }, 7 | { 8 | "title": "Blank Page" 9 | }, 10 | { 11 | "title": "Form" 12 | } 13 | ] 14 | } 15 | } -------------------------------------------------------------------------------- /tests/graphql/test_app_1_get_pages_parent_fail.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | pages(parent: 300) { 3 | title 4 | } 5 | } -------------------------------------------------------------------------------- /tests/graphql/test_app_1_get_pages_parent_fail.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "pages": null 4 | }, 5 | "errors": [ 6 | { 7 | "locations": [ 8 | { 9 | "column": 3, 10 | "line": 2 11 | } 12 | ], 13 | "message": "Page id=300 not found.", 14 | "path": [ 15 | "pages" 16 | ] 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /tests/graphql/test_app_1_get_pages_relay.graphql: -------------------------------------------------------------------------------- 1 | { 2 | pages { 3 | edges { 4 | node { 5 | title 6 | } 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/graphql/test_app_1_get_pages_relay.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "pages": { 4 | "edges": [ 5 | { 6 | "node": { 7 | "title": "Home" 8 | } 9 | }, 10 | { 11 | "node": { 12 | "title": "TEST" 13 | } 14 | }, 15 | { 16 | "node": { 17 | "title": "Blank Page" 18 | } 19 | }, 20 | { 21 | "node": { 22 | "title": "Form" 23 | } 24 | } 25 | ] 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /tests/graphql/test_app_2_streamfield.graphql: -------------------------------------------------------------------------------- 1 | { 2 | page(id: 4) { 3 | __typename 4 | id 5 | title 6 | urlPath 7 | contentType 8 | slug 9 | path 10 | depth 11 | seoTitle 12 | numchild 13 | revision 14 | firstPublishedAt 15 | lastPublishedAt 16 | latestRevisionCreatedAt 17 | live 18 | goLiveAt 19 | expireAt 20 | expired 21 | locked 22 | draftTitle 23 | hasUnpublishedChanges 24 | ... on Test_app_2PageTypeA { 25 | streamfield { 26 | __typename 27 | ... on StringBlock { 28 | val: value 29 | field 30 | } 31 | ... on IntBlock { 32 | value 33 | field 34 | } 35 | } 36 | another { 37 | __typename 38 | ... on StringBlock { 39 | val: value 40 | field 41 | } 42 | ... on IntBlock { 43 | value 44 | field 45 | } 46 | } 47 | third { 48 | __typename 49 | ... on BooleanBlock { 50 | valBool: value 51 | field 52 | } 53 | ... on StringBlock { 54 | valString: value 55 | field 56 | } 57 | ... on IntBlock { 58 | valInt: value 59 | field 60 | } 61 | ... on FloatBlock { 62 | valFloat: value 63 | field 64 | } 65 | ... on DateBlock { 66 | valDate: value 67 | field 68 | } 69 | ... on TimeBlock { 70 | valTime: value 71 | field 72 | } 73 | ... on DateTimeBlock { 74 | valDatetime: value 75 | field 76 | } 77 | } 78 | custom { 79 | __typename 80 | ... on Test_app_2CustomBlock1 { 81 | field 82 | fieldChar 83 | fieldText 84 | fieldEmail 85 | fieldInt 86 | fieldFloat 87 | fieldDecimal 88 | fieldRegex 89 | fieldUrl 90 | fieldBool 91 | fieldDate 92 | fieldTime 93 | fieldDatetime 94 | fieldRich 95 | fieldRaw 96 | fieldQuote 97 | fieldChoice 98 | fieldStatic 99 | fieldList 100 | } 101 | ... on Test_app_2CustomBlock2 { 102 | field 103 | fieldLink { 104 | title 105 | } 106 | fieldLinkList { 107 | title 108 | } 109 | fieldImage { 110 | title 111 | } 112 | fieldImageList { 113 | title 114 | width 115 | } 116 | fieldSnippet { 117 | text 118 | } 119 | fieldSnippetList { 120 | text 121 | } 122 | } 123 | } 124 | links { 125 | __typename 126 | ... on PageBlock { 127 | value { 128 | title 129 | id 130 | } 131 | field 132 | } 133 | ... on ImageBlock { 134 | value { 135 | title 136 | width 137 | } 138 | field 139 | } 140 | ... on Test_app_2App2SnippetBlock { 141 | value { 142 | text 143 | } 144 | field 145 | } 146 | } 147 | lists { 148 | __typename 149 | ... on StringListBlock { 150 | value 151 | field 152 | } 153 | ... on DateTimeListBlock { 154 | valDateTime: value 155 | field 156 | } 157 | ... on TimeListBlock { 158 | valTime: value 159 | field 160 | } 161 | ... on DateListBlock { 162 | valDate: value 163 | field 164 | } 165 | ... on FloatListBlock { 166 | valFloat: value 167 | field 168 | } 169 | ... on IntListBlock { 170 | valInt: value 171 | field 172 | } 173 | } 174 | 175 | linksList { 176 | __typename 177 | ... on ImageListBlock { 178 | value { 179 | title 180 | width 181 | } 182 | field 183 | } 184 | ... on PageListBlock { 185 | value { 186 | title 187 | } 188 | field 189 | } 190 | ... on Test_app_2App2SnippetListBlock { 191 | value { 192 | text 193 | } 194 | field 195 | } 196 | } 197 | 198 | customLists { 199 | __typename 200 | ... on Test_app_2CustomBlock1ListBlock { 201 | value { 202 | fieldChar 203 | fieldText 204 | fieldDecimal 205 | fieldDate 206 | fieldDatetime 207 | fieldTime 208 | } 209 | field 210 | } 211 | } 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /tests/graphql/test_app_2_streamfield.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "page": { 4 | "__typename": "Test_app_2PageTypeA", 5 | "another": [ 6 | { 7 | "__typename": "StringBlock", 8 | "field": "h4", 9 | "val": "and H4" 10 | }, 11 | { 12 | "__typename": "IntBlock", 13 | "field": "n2", 14 | "value": 1000 15 | }, 16 | { 17 | "__typename": "StringBlock", 18 | "field": "h3", 19 | "val": "or H3" 20 | } 21 | ], 22 | "contentType": "test_app_2.PageTypeA", 23 | "custom": [ 24 | { 25 | "__typename": "Test_app_2CustomBlock1", 26 | "field": "custom1", 27 | "fieldBool": true, 28 | "fieldChar": "sdfsdh", 29 | "fieldChoice": "coffee", 30 | "fieldDate": "2019-01-02", 31 | "fieldDatetime": "2019-01-08T16:00:00+00:00", 32 | "fieldDecimal": "2323.23", 33 | "fieldEmail": "dsfsdf@sdsdf.com", 34 | "fieldFloat": 23324.21232, 35 | "fieldInt": 324324, 36 | "fieldList": [ 37 | "wwewer", 38 | "dsfsdsdf" 39 | ], 40 | "fieldQuote": "jhdsfjksdhfjkdshf", 41 | "fieldRaw": "

test

", 42 | "fieldRegex": "234", 43 | "fieldRich": "

ffdsdfgdfjg

dsfsfsd

sdfhsd

", 44 | "fieldStatic": null, 45 | "fieldText": "sdjhfsdkjfsdkjh", 46 | "fieldTime": "20:00:00", 47 | "fieldUrl": "http://www.google.com" 48 | }, 49 | { 50 | "__typename": "Test_app_2CustomBlock2", 51 | "field": "custom2", 52 | "fieldImage": { 53 | "title": "wagtail logo with focal point" 54 | }, 55 | "fieldImageList": [ 56 | { 57 | "title": "wagtail logo with focal point", 58 | "width": 200 59 | }, 60 | { 61 | "title": "wagtail logo", 62 | "width": 200 63 | } 64 | ], 65 | "fieldLink": { 66 | "title": "Home" 67 | }, 68 | "fieldLinkList": [ 69 | { 70 | "title": "Home" 71 | }, 72 | { 73 | "title": "TEST" 74 | } 75 | ], 76 | "fieldSnippet": { 77 | "text": "sdjfkhdskjsdf" 78 | }, 79 | "fieldSnippetList": [ 80 | { 81 | "text": "sdjfkhdskjsdf" 82 | }, 83 | { 84 | "text": "09dfjklwf" 85 | } 86 | ] 87 | } 88 | ], 89 | "customLists": [ 90 | { 91 | "__typename": "Test_app_2CustomBlock1ListBlock", 92 | "field": "custom1", 93 | "value": [ 94 | { 95 | "fieldChar": "dssdf", 96 | "fieldDate": "2019-01-08", 97 | "fieldDatetime": "2019-01-18T22:40:00+00:00", 98 | "fieldDecimal": "23.232", 99 | "fieldText": "ssdf", 100 | "fieldTime": null 101 | } 102 | ] 103 | } 104 | ], 105 | "depth": 3, 106 | "draftTitle": "TEST", 107 | "expireAt": null, 108 | "expired": false, 109 | "firstPublishedAt": "2019-01-05T06:32:37.170794+00:00", 110 | "goLiveAt": null, 111 | "hasUnpublishedChanges": false, 112 | "id": 4, 113 | "lastPublishedAt": "2019-01-06T06:40:49.336072+00:00", 114 | "latestRevisionCreatedAt": "2019-01-06T06:40:49.268890+00:00", 115 | "links": [ 116 | { 117 | "__typename": "ImageBlock", 118 | "field": "image", 119 | "value": { 120 | "title": "wagtail logo with focal point", 121 | "width": 200 122 | } 123 | }, 124 | { 125 | "__typename": "PageBlock", 126 | "field": "page", 127 | "value": { 128 | "id": 3, 129 | "title": "Home" 130 | } 131 | }, 132 | { 133 | "__typename": "Test_app_2App2SnippetBlock", 134 | "field": "snippet", 135 | "value": { 136 | "text": "sdjfkhdskjsdf" 137 | } 138 | }, 139 | { 140 | "__typename": "ImageBlock", 141 | "field": "image", 142 | "value": { 143 | "title": "wagtail logo", 144 | "width": 200 145 | } 146 | } 147 | ], 148 | "linksList": [ 149 | { 150 | "__typename": "ImageListBlock", 151 | "field": "image", 152 | "value": [ 153 | { 154 | "title": "wagtail logo with focal point", 155 | "width": 200 156 | }, 157 | { 158 | "title": "wagtail logo", 159 | "width": 200 160 | } 161 | ] 162 | }, 163 | { 164 | "__typename": "PageListBlock", 165 | "field": "page", 166 | "value": [ 167 | { 168 | "title": "Home" 169 | }, 170 | { 171 | "title": "TEST" 172 | } 173 | ] 174 | }, 175 | { 176 | "__typename": "Test_app_2App2SnippetListBlock", 177 | "field": "snippet", 178 | "value": [ 179 | { 180 | "text": "sdjfkhdskjsdf" 181 | }, 182 | { 183 | "text": "09dfjklwf" 184 | } 185 | ] 186 | } 187 | ], 188 | "lists": [ 189 | { 190 | "__typename": "StringListBlock", 191 | "field": "char", 192 | "value": [ 193 | "c1", 194 | "c2" 195 | ] 196 | }, 197 | { 198 | "__typename": "StringListBlock", 199 | "field": "text", 200 | "value": [ 201 | "t1", 202 | "t2" 203 | ] 204 | }, 205 | { 206 | "__typename": "IntListBlock", 207 | "field": "int", 208 | "valInt": [ 209 | 3, 210 | 6 211 | ] 212 | }, 213 | { 214 | "__typename": "FloatListBlock", 215 | "field": "float", 216 | "valFloat": [ 217 | 3463.234, 218 | 4345.324 219 | ] 220 | }, 221 | { 222 | "__typename": "StringListBlock", 223 | "field": "decimal", 224 | "value": [ 225 | "12.4" 226 | ] 227 | }, 228 | { 229 | "__typename": "DateListBlock", 230 | "field": "date", 231 | "valDate": [ 232 | "2019-01-16", 233 | "2019-01-28" 234 | ] 235 | }, 236 | { 237 | "__typename": "TimeListBlock", 238 | "field": "time", 239 | "valTime": [ 240 | "20:00:00", 241 | "23:00:00" 242 | ] 243 | }, 244 | { 245 | "__typename": "DateTimeListBlock", 246 | "field": "datetime", 247 | "valDateTime": [ 248 | "2019-01-05T20:00:00+00:00", 249 | "2019-01-08T19:00:00+00:00" 250 | ] 251 | } 252 | ], 253 | "live": true, 254 | "locked": false, 255 | "numchild": 0, 256 | "path": "000100010001", 257 | "revision": null, 258 | "seoTitle": null, 259 | "slug": "test", 260 | "streamfield": [ 261 | { 262 | "__typename": "StringBlock", 263 | "field": "h1", 264 | "val": "This is H1" 265 | }, 266 | { 267 | "__typename": "StringBlock", 268 | "field": "h2", 269 | "val": "H2 Title" 270 | }, 271 | { 272 | "__typename": "IntBlock", 273 | "field": "n1", 274 | "value": 93 275 | } 276 | ], 277 | "third": [ 278 | { 279 | "__typename": "StringBlock", 280 | "field": "char", 281 | "valString": "asgdsahd" 282 | }, 283 | { 284 | "__typename": "StringBlock", 285 | "field": "text", 286 | "valString": "sdfsdfsdfsdf\r\nsdfsdffsdf" 287 | }, 288 | { 289 | "__typename": "StringBlock", 290 | "field": "email", 291 | "valString": "user@user.com" 292 | }, 293 | { 294 | "__typename": "IntBlock", 295 | "field": "int", 296 | "valInt": 234318 297 | }, 298 | { 299 | "__typename": "FloatBlock", 300 | "field": "float", 301 | "valFloat": 234234.43324 302 | }, 303 | { 304 | "__typename": "StringBlock", 305 | "field": "decimal", 306 | "valString": "234234.324" 307 | }, 308 | { 309 | "__typename": "StringBlock", 310 | "field": "regex", 311 | "valString": "131" 312 | }, 313 | { 314 | "__typename": "StringBlock", 315 | "field": "url", 316 | "valString": "https://www.google.com" 317 | }, 318 | { 319 | "__typename": "BooleanBlock", 320 | "field": "bool", 321 | "valBool": true 322 | }, 323 | { 324 | "__typename": "DateBlock", 325 | "field": "date", 326 | "valDate": "2019-01-17" 327 | }, 328 | { 329 | "__typename": "TimeBlock", 330 | "field": "time", 331 | "valTime": "12:00:00" 332 | }, 333 | { 334 | "__typename": "DateTimeBlock", 335 | "field": "datetime", 336 | "valDatetime": "2019-01-24T13:00:00+00:00" 337 | }, 338 | { 339 | "__typename": "StringBlock", 340 | "field": "rich", 341 | "valString": "

fdgfdfdg

  1. f
  2. d
  3. e
  4. w

2 sdfsdfsdf

Home

" 342 | }, 343 | { 344 | "__typename": "StringBlock", 345 | "field": "raw", 346 | "valString": "

this is a paragraph

" 347 | }, 348 | { 349 | "__typename": "StringBlock", 350 | "field": "quote", 351 | "valString": "quote test" 352 | }, 353 | { 354 | "__typename": "StringBlock", 355 | "field": "choice", 356 | "valString": "tea" 357 | }, 358 | { 359 | "__typename": "StringBlock", 360 | "field": "static", 361 | "valString": null 362 | } 363 | ], 364 | "title": "TEST", 365 | "urlPath": "/test" 366 | } 367 | } 368 | } -------------------------------------------------------------------------------- /tests/graphql/test_app_2_streamfield_relay.graphql: -------------------------------------------------------------------------------- 1 | { 2 | page(id: 4) { 3 | __typename 4 | id 5 | title 6 | urlPath 7 | contentType 8 | slug 9 | path 10 | depth 11 | seoTitle 12 | numchild 13 | revision 14 | firstPublishedAt 15 | lastPublishedAt 16 | latestRevisionCreatedAt 17 | live 18 | goLiveAt 19 | expireAt 20 | expired 21 | locked 22 | draftTitle 23 | hasUnpublishedChanges 24 | ... on Test_app_2PageTypeA { 25 | streamfield { 26 | __typename 27 | ... on StringBlock { 28 | val: value 29 | field 30 | } 31 | ... on IntBlock { 32 | value 33 | field 34 | } 35 | } 36 | another { 37 | __typename 38 | ... on StringBlock { 39 | val: value 40 | field 41 | } 42 | ... on IntBlock { 43 | value 44 | field 45 | } 46 | } 47 | third { 48 | __typename 49 | ... on BooleanBlock { 50 | valBool: value 51 | field 52 | } 53 | ... on StringBlock { 54 | valString: value 55 | field 56 | } 57 | ... on IntBlock { 58 | valInt: value 59 | field 60 | } 61 | ... on FloatBlock { 62 | valFloat: value 63 | field 64 | } 65 | ... on DateBlock { 66 | valDate: value 67 | field 68 | } 69 | ... on TimeBlock { 70 | valTime: value 71 | field 72 | } 73 | ... on DateTimeBlock { 74 | valDatetime: value 75 | field 76 | } 77 | } 78 | custom { 79 | __typename 80 | ... on Test_app_2CustomBlock1 { 81 | field 82 | fieldChar 83 | fieldText 84 | fieldEmail 85 | fieldInt 86 | fieldFloat 87 | fieldDecimal 88 | fieldRegex 89 | fieldUrl 90 | fieldBool 91 | fieldDate 92 | fieldTime 93 | fieldDatetime 94 | fieldRich 95 | fieldRaw 96 | fieldQuote 97 | fieldChoice 98 | fieldStatic 99 | fieldList 100 | } 101 | ... on Test_app_2CustomBlock2 { 102 | field 103 | fieldLink { 104 | title 105 | } 106 | fieldLinkList { 107 | title 108 | } 109 | fieldImage { 110 | title 111 | } 112 | fieldImageList { 113 | title 114 | width 115 | } 116 | fieldSnippet { 117 | text 118 | } 119 | fieldSnippetList { 120 | text 121 | } 122 | } 123 | } 124 | links { 125 | __typename 126 | ... on PageBlock { 127 | value { 128 | title 129 | id 130 | } 131 | field 132 | } 133 | ... on ImageBlock { 134 | value { 135 | title 136 | width 137 | } 138 | field 139 | } 140 | ... on Test_app_2App2SnippetBlock { 141 | value { 142 | text 143 | } 144 | field 145 | } 146 | } 147 | lists { 148 | __typename 149 | ... on StringListBlock { 150 | value 151 | field 152 | } 153 | ... on DateTimeListBlock { 154 | valDateTime: value 155 | field 156 | } 157 | ... on TimeListBlock { 158 | valTime: value 159 | field 160 | } 161 | ... on DateListBlock { 162 | valDate: value 163 | field 164 | } 165 | ... on FloatListBlock { 166 | valFloat: value 167 | field 168 | } 169 | ... on IntListBlock { 170 | valInt: value 171 | field 172 | } 173 | } 174 | 175 | linksList { 176 | __typename 177 | ... on ImageListBlock { 178 | value { 179 | title 180 | width 181 | } 182 | field 183 | } 184 | ... on PageListBlock { 185 | value { 186 | title 187 | } 188 | field 189 | } 190 | ... on Test_app_2App2SnippetListBlock { 191 | value { 192 | text 193 | } 194 | field 195 | } 196 | } 197 | 198 | customLists { 199 | __typename 200 | ... on Test_app_2CustomBlock1ListBlock { 201 | value { 202 | fieldChar 203 | fieldText 204 | fieldDecimal 205 | fieldDate 206 | fieldDatetime 207 | fieldTime 208 | } 209 | field 210 | } 211 | } 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /tests/graphql/test_app_2_streamfield_relay.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "page": { 4 | "__typename": "Test_app_2PageTypeA", 5 | "another": [ 6 | { 7 | "__typename": "StringBlock", 8 | "field": "h4", 9 | "val": "and H4" 10 | }, 11 | { 12 | "__typename": "IntBlock", 13 | "field": "n2", 14 | "value": 1000 15 | }, 16 | { 17 | "__typename": "StringBlock", 18 | "field": "h3", 19 | "val": "or H3" 20 | } 21 | ], 22 | "contentType": "test_app_2.PageTypeA", 23 | "custom": [ 24 | { 25 | "__typename": "Test_app_2CustomBlock1", 26 | "field": "custom1", 27 | "fieldBool": true, 28 | "fieldChar": "sdfsdh", 29 | "fieldChoice": "coffee", 30 | "fieldDate": "2019-01-02", 31 | "fieldDatetime": "2019-01-08T16:00:00+00:00", 32 | "fieldDecimal": "2323.23", 33 | "fieldEmail": "dsfsdf@sdsdf.com", 34 | "fieldFloat": 23324.21232, 35 | "fieldInt": 324324, 36 | "fieldList": [ 37 | "wwewer", 38 | "dsfsdsdf" 39 | ], 40 | "fieldQuote": "jhdsfjksdhfjkdshf", 41 | "fieldRaw": "

test

", 42 | "fieldRegex": "234", 43 | "fieldRich": "

ffdsdfgdfjg

dsfsfsd

sdfhsd

", 44 | "fieldStatic": null, 45 | "fieldText": "sdjhfsdkjfsdkjh", 46 | "fieldTime": "20:00:00", 47 | "fieldUrl": "http://www.google.com" 48 | }, 49 | { 50 | "__typename": "Test_app_2CustomBlock2", 51 | "field": "custom2", 52 | "fieldImage": { 53 | "title": "wagtail logo with focal point" 54 | }, 55 | "fieldImageList": [ 56 | { 57 | "title": "wagtail logo with focal point", 58 | "width": 200 59 | }, 60 | { 61 | "title": "wagtail logo", 62 | "width": 200 63 | } 64 | ], 65 | "fieldLink": { 66 | "title": "Home" 67 | }, 68 | "fieldLinkList": [ 69 | { 70 | "title": "Home" 71 | }, 72 | { 73 | "title": "TEST" 74 | } 75 | ], 76 | "fieldSnippet": { 77 | "text": "sdjfkhdskjsdf" 78 | }, 79 | "fieldSnippetList": [ 80 | { 81 | "text": "sdjfkhdskjsdf" 82 | }, 83 | { 84 | "text": "09dfjklwf" 85 | } 86 | ] 87 | } 88 | ], 89 | "customLists": [ 90 | { 91 | "__typename": "Test_app_2CustomBlock1ListBlock", 92 | "field": "custom1", 93 | "value": [ 94 | { 95 | "fieldChar": "dssdf", 96 | "fieldDate": "2019-01-08", 97 | "fieldDatetime": "2019-01-18T22:40:00+00:00", 98 | "fieldDecimal": "23.232", 99 | "fieldText": "ssdf", 100 | "fieldTime": null 101 | } 102 | ] 103 | } 104 | ], 105 | "depth": 3, 106 | "draftTitle": "TEST", 107 | "expireAt": null, 108 | "expired": false, 109 | "firstPublishedAt": "2019-01-05T06:32:37.170794+00:00", 110 | "goLiveAt": null, 111 | "hasUnpublishedChanges": false, 112 | "id": "VGVzdF9hcHBfMlBhZ2VUeXBlQTo0", 113 | "lastPublishedAt": "2019-01-06T06:40:49.336072+00:00", 114 | "latestRevisionCreatedAt": "2019-01-06T06:40:49.268890+00:00", 115 | "links": [ 116 | { 117 | "__typename": "ImageBlock", 118 | "field": "image", 119 | "value": { 120 | "title": "wagtail logo with focal point", 121 | "width": 200 122 | } 123 | }, 124 | { 125 | "__typename": "PageBlock", 126 | "field": "page", 127 | "value": { 128 | "id": "VGVzdF9hcHBfMUhvbWVQYWdlOjM=", 129 | "title": "Home" 130 | } 131 | }, 132 | { 133 | "__typename": "Test_app_2App2SnippetBlock", 134 | "field": "snippet", 135 | "value": { 136 | "text": "sdjfkhdskjsdf" 137 | } 138 | }, 139 | { 140 | "__typename": "ImageBlock", 141 | "field": "image", 142 | "value": { 143 | "title": "wagtail logo", 144 | "width": 200 145 | } 146 | } 147 | ], 148 | "linksList": [ 149 | { 150 | "__typename": "ImageListBlock", 151 | "field": "image", 152 | "value": [ 153 | { 154 | "title": "wagtail logo with focal point", 155 | "width": 200 156 | }, 157 | { 158 | "title": "wagtail logo", 159 | "width": 200 160 | } 161 | ] 162 | }, 163 | { 164 | "__typename": "PageListBlock", 165 | "field": "page", 166 | "value": [ 167 | { 168 | "title": "Home" 169 | }, 170 | { 171 | "title": "TEST" 172 | } 173 | ] 174 | }, 175 | { 176 | "__typename": "Test_app_2App2SnippetListBlock", 177 | "field": "snippet", 178 | "value": [ 179 | { 180 | "text": "sdjfkhdskjsdf" 181 | }, 182 | { 183 | "text": "09dfjklwf" 184 | } 185 | ] 186 | } 187 | ], 188 | "lists": [ 189 | { 190 | "__typename": "StringListBlock", 191 | "field": "char", 192 | "value": [ 193 | "c1", 194 | "c2" 195 | ] 196 | }, 197 | { 198 | "__typename": "StringListBlock", 199 | "field": "text", 200 | "value": [ 201 | "t1", 202 | "t2" 203 | ] 204 | }, 205 | { 206 | "__typename": "IntListBlock", 207 | "field": "int", 208 | "valInt": [ 209 | 3, 210 | 6 211 | ] 212 | }, 213 | { 214 | "__typename": "FloatListBlock", 215 | "field": "float", 216 | "valFloat": [ 217 | 3463.234, 218 | 4345.324 219 | ] 220 | }, 221 | { 222 | "__typename": "StringListBlock", 223 | "field": "decimal", 224 | "value": [ 225 | "12.4" 226 | ] 227 | }, 228 | { 229 | "__typename": "DateListBlock", 230 | "field": "date", 231 | "valDate": [ 232 | "2019-01-16", 233 | "2019-01-28" 234 | ] 235 | }, 236 | { 237 | "__typename": "TimeListBlock", 238 | "field": "time", 239 | "valTime": [ 240 | "20:00:00", 241 | "23:00:00" 242 | ] 243 | }, 244 | { 245 | "__typename": "DateTimeListBlock", 246 | "field": "datetime", 247 | "valDateTime": [ 248 | "2019-01-05T20:00:00+00:00", 249 | "2019-01-08T19:00:00+00:00" 250 | ] 251 | } 252 | ], 253 | "live": true, 254 | "locked": false, 255 | "numchild": 0, 256 | "path": "000100010001", 257 | "revision": null, 258 | "seoTitle": null, 259 | "slug": "test", 260 | "streamfield": [ 261 | { 262 | "__typename": "StringBlock", 263 | "field": "h1", 264 | "val": "This is H1" 265 | }, 266 | { 267 | "__typename": "StringBlock", 268 | "field": "h2", 269 | "val": "H2 Title" 270 | }, 271 | { 272 | "__typename": "IntBlock", 273 | "field": "n1", 274 | "value": 93 275 | } 276 | ], 277 | "third": [ 278 | { 279 | "__typename": "StringBlock", 280 | "field": "char", 281 | "valString": "asgdsahd" 282 | }, 283 | { 284 | "__typename": "StringBlock", 285 | "field": "text", 286 | "valString": "sdfsdfsdfsdf\r\nsdfsdffsdf" 287 | }, 288 | { 289 | "__typename": "StringBlock", 290 | "field": "email", 291 | "valString": "user@user.com" 292 | }, 293 | { 294 | "__typename": "IntBlock", 295 | "field": "int", 296 | "valInt": 234318 297 | }, 298 | { 299 | "__typename": "FloatBlock", 300 | "field": "float", 301 | "valFloat": 234234.43324 302 | }, 303 | { 304 | "__typename": "StringBlock", 305 | "field": "decimal", 306 | "valString": "234234.324" 307 | }, 308 | { 309 | "__typename": "StringBlock", 310 | "field": "regex", 311 | "valString": "131" 312 | }, 313 | { 314 | "__typename": "StringBlock", 315 | "field": "url", 316 | "valString": "https://www.google.com" 317 | }, 318 | { 319 | "__typename": "BooleanBlock", 320 | "field": "bool", 321 | "valBool": true 322 | }, 323 | { 324 | "__typename": "DateBlock", 325 | "field": "date", 326 | "valDate": "2019-01-17" 327 | }, 328 | { 329 | "__typename": "TimeBlock", 330 | "field": "time", 331 | "valTime": "12:00:00" 332 | }, 333 | { 334 | "__typename": "DateTimeBlock", 335 | "field": "datetime", 336 | "valDatetime": "2019-01-24T13:00:00+00:00" 337 | }, 338 | { 339 | "__typename": "StringBlock", 340 | "field": "rich", 341 | "valString": "

fdgfdfdg

  1. f
  2. d
  3. e
  4. w

2 sdfsdfsdf

Home

" 342 | }, 343 | { 344 | "__typename": "StringBlock", 345 | "field": "raw", 346 | "valString": "

this is a paragraph

" 347 | }, 348 | { 349 | "__typename": "StringBlock", 350 | "field": "quote", 351 | "valString": "quote test" 352 | }, 353 | { 354 | "__typename": "StringBlock", 355 | "field": "choice", 356 | "valString": "tea" 357 | }, 358 | { 359 | "__typename": "StringBlock", 360 | "field": "static", 361 | "valString": null 362 | } 363 | ], 364 | "title": "TEST", 365 | "urlPath": "/test" 366 | } 367 | } 368 | } -------------------------------------------------------------------------------- /tests/graphql/test_user_admin.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | user { 3 | id 4 | isSuperuser 5 | username 6 | firstName 7 | lastName 8 | email 9 | isStaff 10 | isActive 11 | dateJoined 12 | } 13 | } -------------------------------------------------------------------------------- /tests/graphql/test_user_admin.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "user": { 4 | "dateJoined": "2019-01-04T23:51:26.589548+00:00", 5 | "email": "admin@admin.org", 6 | "firstName": "First", 7 | "id": "1", 8 | "isActive": true, 9 | "isStaff": true, 10 | "isSuperuser": true, 11 | "lastName": "Last", 12 | "username": "admin" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /tests/graphql/test_user_anonymous.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | user { 3 | id 4 | isSuperuser 5 | username 6 | firstName 7 | lastName 8 | email 9 | isStaff 10 | isActive 11 | } 12 | } -------------------------------------------------------------------------------- /tests/graphql/test_user_anonymous.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "user": { 4 | "id": "-1", 5 | "isSuperuser": false, 6 | "username": "anonymous", 7 | "firstName": "", 8 | "lastName": "", 9 | "email": "", 10 | "isStaff": false, 11 | "isActive": true 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/test_auth.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from .graphql import assert_query 3 | 4 | 5 | @pytest.mark.django_db 6 | def test_get_user(client): 7 | assert_query(client, 'test_user', 'anonymous') 8 | 9 | 10 | @pytest.mark.django_db 11 | def test_get_user_admin(client): 12 | client.post('/graphql', 13 | {"query": 'mutation { login(username: "admin", password: "password") { user { username } } }'} 14 | ) 15 | assert_query(client, 'test_user', 'admin') 16 | response = client.post( 17 | '/graphql', 18 | {"query": 'mutation { logout { user { username } } }'} 19 | ) 20 | assert response.json() == {'data': {'logout': {'user': {'username': 'anonymous'}}}} 21 | 22 | 23 | @pytest.mark.django_db 24 | def test_get_user_admin_fail(client): 25 | client.post('/graphql', 26 | {"query": 'mutation { login(username: "admin", password: "fail") { user { username } } }'} 27 | ) 28 | assert_query(client, 'test_user', 'anonymous') 29 | -------------------------------------------------------------------------------- /tests/test_basic_pages.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from .graphql import assert_query, assert_query_fail 3 | from django.conf import settings 4 | IS_RELAY = settings.GRAPHQL_API.get('RELAY', False) 5 | 6 | 7 | @pytest.mark.django_db 8 | def test_type(client): 9 | response = client.post('/graphql', {"query": "{__schema { types { name kind } } }"}) 10 | assert response.status_code == 200 11 | assert {'name': 'Test_app_1HomePage', 'kind': 'OBJECT'} in response.json()['data']['__schema']['types'] 12 | 13 | 14 | @pytest.mark.skipif(IS_RELAY, reason="requires the relay setting") 15 | @pytest.mark.django_db 16 | def test_get_page(client): 17 | assert_query(client, 'test_app_1', 'get_home') 18 | 19 | 20 | @pytest.mark.skipif(not IS_RELAY, reason="requires the relay setting to be off") 21 | @pytest.mark.django_db 22 | def test_get_page_relay(client): 23 | assert_query(client, 'test_app_1', 'get_home', 'relay') 24 | 25 | 26 | @pytest.mark.skipif(IS_RELAY, reason="requires the relay setting") 27 | @pytest.mark.django_db 28 | def test_get_page_latest(client): 29 | assert_query(client, 'test_app_1', 'get_home', 'latest') 30 | 31 | 32 | @pytest.mark.skipif(not IS_RELAY, reason="requires the relay setting to be off") 33 | @pytest.mark.django_db 34 | def test_get_page_latest_relay(client): 35 | assert_query(client, 'test_app_1', 'get_home', 'latest', 'relay') 36 | 37 | 38 | @pytest.mark.skipif(IS_RELAY, reason="requires the relay setting") 39 | @pytest.mark.django_db 40 | def test_get_page_revision(client): 41 | assert_query(client, 'test_app_1', 'get_home', 'revision') 42 | 43 | 44 | @pytest.mark.skipif(not IS_RELAY, reason="requires the relay setting to be off") 45 | @pytest.mark.django_db 46 | def test_get_page_revision_relay(client): 47 | assert_query(client, 'test_app_1', 'get_home', 'revision', 'relay') 48 | 49 | 50 | @pytest.mark.django_db 51 | def test_get_page_doesnt_exist(client): 52 | assert_query(client, 'test_app_1', 'get_home', 'none') 53 | 54 | 55 | @pytest.mark.django_db 56 | def test_get_page_revision_doesnt_exist(client): 57 | assert_query_fail(client, 'test_app_1', 'get_home', 'revision', 'fail') 58 | 59 | 60 | @pytest.mark.skipif(IS_RELAY, reason="requires the relay setting") 61 | @pytest.mark.django_db 62 | def test_get_pages(client): 63 | assert_query(client, 'test_app_1', 'get', 'pages') 64 | 65 | 66 | @pytest.mark.skipif(not IS_RELAY, reason="requires the relay setting to be off") 67 | @pytest.mark.django_db 68 | def test_get_pages_relay(client): 69 | assert_query(client, 'test_app_1', 'get', 'pages', 'relay') 70 | 71 | 72 | @pytest.mark.skipif(IS_RELAY, reason="requires the relay setting") 73 | @pytest.mark.django_db 74 | def test_get_pages_parent(client): 75 | assert_query(client, 'test_app_1', 'get', 'pages', 'parent') 76 | 77 | 78 | @pytest.mark.skipif(IS_RELAY, reason="requires the relay setting") 79 | @pytest.mark.django_db 80 | def test_get_pages_parent_fail(client): 81 | assert_query_fail(client, 'test_app_1', 'get', 'pages', 'parent', 'fail') 82 | 83 | 84 | @pytest.mark.skipif(IS_RELAY, reason="requires the relay setting") 85 | @pytest.mark.django_db 86 | def test_get_children(client): 87 | assert_query(client, 'children') 88 | 89 | 90 | @pytest.mark.skipif(IS_RELAY, reason="requires the relay setting") 91 | @pytest.mark.django_db 92 | def test_show_in_menus(client): 93 | assert_query(client, 'showmenus') 94 | 95 | 96 | @pytest.mark.skipif(IS_RELAY, reason="requires the relay setting") 97 | @pytest.mark.django_db 98 | def test_prefetch(client): 99 | assert_query(client, 'prefetch') 100 | -------------------------------------------------------------------------------- /tests/test_documents.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from .graphql import assert_query 3 | 4 | 5 | @pytest.mark.django_db 6 | def test_documents(client): 7 | assert_query(client, 'documents', 'all') 8 | 9 | 10 | @pytest.mark.django_db 11 | def test_document_1(client): 12 | assert_query(client, 'document', '1') 13 | -------------------------------------------------------------------------------- /tests/test_forms.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import json 3 | 4 | 5 | @pytest.mark.django_db 6 | def test_form_1(client): 7 | query = """ 8 | mutation($values: GenericScalar) { 9 | testApp1FormPage(url: "/form", values: $values) { 10 | result 11 | errors { 12 | name 13 | errors 14 | } 15 | } 16 | } 17 | """ 18 | variables = json.dumps({ 19 | "values": {"label-1": "1", "other-label": 2} 20 | }) 21 | 22 | response = client.post('/graphql', {"query": query, "variables": variables}) 23 | print(response) 24 | print(response.json()) 25 | assert response.status_code == 200 26 | response_json = response.json() 27 | assert response_json['data']['testApp1FormPage']['errors'] is None 28 | assert response_json['data']['testApp1FormPage']['result'] == 'OK' 29 | 30 | 31 | @pytest.mark.django_db 32 | def test_form_fail_1(client): 33 | query = """ 34 | mutation($values: GenericScalar) { 35 | testApp1FormPage(url: "/form", values: $values) { 36 | result 37 | errors { 38 | name 39 | errors 40 | } 41 | } 42 | } 43 | """ 44 | variables = json.dumps({ 45 | "values": {"label-fail": "1", "other-label": 2} 46 | }) 47 | 48 | response = client.post('/graphql', {"query": query, "variables": variables}) 49 | print(response) 50 | print(response.json()) 51 | assert response.status_code == 200 52 | response_json = response.json() 53 | assert response_json['data']['testApp1FormPage']['errors'] is not None 54 | assert {'name': 'label-1', 'errors': ['This field is required.']}\ 55 | in response_json['data']['testApp1FormPage']['errors'] 56 | assert response_json['data']['testApp1FormPage']['result'] == 'FAIL' 57 | 58 | 59 | @pytest.mark.django_db 60 | def test_form_fields(client): 61 | query = """ 62 | query { 63 | page(id: 6) { 64 | ... on Test_app_1FormPage { 65 | formFields { 66 | name 67 | fieldType 68 | helpText 69 | required 70 | choices 71 | defaultValue 72 | label 73 | } 74 | } 75 | } 76 | } 77 | """ 78 | response = client.post('/graphql', {"query": query}) 79 | print(response) 80 | print(response.json()) 81 | 82 | assert response.json() == { 83 | "data": { 84 | "page": { 85 | "formFields": [ 86 | { 87 | "choices": "", 88 | "defaultValue": "default", 89 | "fieldType": "singleline", 90 | "helpText": "Help", 91 | "label": "Label 1", 92 | "name": "label-1", 93 | "required": True 94 | }, 95 | { 96 | "choices": "", 97 | "defaultValue": "1", 98 | "fieldType": "number", 99 | "helpText": "", 100 | "label": "other label", 101 | "name": "other-label", 102 | "required": False 103 | } 104 | ] 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/test_images.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from .graphql import assert_query 3 | 4 | 5 | @pytest.mark.django_db 6 | def test_image(client): 7 | assert_query(client, 'image', '1') 8 | 9 | 10 | @pytest.mark.django_db 11 | def test_image_with_focal_point(client): 12 | assert_query(client, 'image', '2') 13 | 14 | 15 | @pytest.mark.django_db 16 | def test_image_with_rendition(client): 17 | assert_query(client, 'image', '1', 'rendition') 18 | 19 | 20 | @pytest.mark.django_db 21 | def test_images_list(client): 22 | assert_query(client, 'images', 'all') 23 | -------------------------------------------------------------------------------- /tests/test_menus.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from .graphql import assert_query 3 | 4 | 5 | @pytest.mark.django_db 6 | def test_documents(client): 7 | assert_query(client, 'menus', 'all') 8 | -------------------------------------------------------------------------------- /tests/test_permissions.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tr11/wagtail-graphql/1c10172f2fb11a5568a8099de00ed2afb59340d7/tests/test_permissions.py -------------------------------------------------------------------------------- /tests/test_project/.gitignore: -------------------------------------------------------------------------------- 1 | media 2 | migrations 3 | -------------------------------------------------------------------------------- /tests/test_project/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tr11/wagtail-graphql/1c10172f2fb11a5568a8099de00ed2afb59340d7/tests/test_project/db.sqlite3 -------------------------------------------------------------------------------- /tests/test_project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings.test") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /tests/test_project/project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tr11/wagtail-graphql/1c10172f2fb11a5568a8099de00ed2afb59340d7/tests/test_project/project/__init__.py -------------------------------------------------------------------------------- /tests/test_project/project/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tr11/wagtail-graphql/1c10172f2fb11a5568a8099de00ed2afb59340d7/tests/test_project/project/settings/__init__.py -------------------------------------------------------------------------------- /tests/test_project/project/settings/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for project project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.1.3. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.1/ref/settings/ 11 | """ 12 | 13 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 14 | import os 15 | 16 | PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | BASE_DIR = os.path.dirname(PROJECT_DIR) 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ 22 | 23 | 24 | # Application definition 25 | 26 | INSTALLED_APPS = [ 27 | 'test_app_1', 28 | 'test_app_2', 29 | 30 | 'wagtail_graphql', 31 | 'graphene_django', 32 | 33 | 'wagtail.contrib.modeladmin', 34 | 'wagtailmenus', 35 | 36 | 'wagtail.contrib.settings', 37 | 38 | 'wagtail.contrib.forms', 39 | 'wagtail.contrib.redirects', 40 | 'wagtail.embeds', 41 | 'wagtail.sites', 42 | 'wagtail.users', 43 | 'wagtail.snippets', 44 | 'wagtail.documents', 45 | 'wagtail.images', 46 | 'wagtail.search', 47 | 'wagtail.admin', 48 | 'wagtail.core', 49 | 50 | 'corsheaders', 51 | 'modelcluster', 52 | 'taggit', 53 | 54 | 'django.contrib.admin', 55 | 'django.contrib.auth', 56 | 'django.contrib.contenttypes', 57 | 'django.contrib.sessions', 58 | 'django.contrib.messages', 59 | 'django.contrib.staticfiles', 60 | ] 61 | 62 | MIDDLEWARE = [ 63 | 'corsheaders.middleware.CorsMiddleware', 64 | 'django.contrib.sessions.middleware.SessionMiddleware', 65 | 'django.middleware.common.CommonMiddleware', 66 | 'django.middleware.csrf.CsrfViewMiddleware', 67 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 68 | 'django.contrib.messages.middleware.MessageMiddleware', 69 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 70 | 'django.middleware.security.SecurityMiddleware', 71 | 72 | 'wagtail.core.middleware.SiteMiddleware', 73 | 'wagtail.contrib.redirects.middleware.RedirectMiddleware', 74 | ] 75 | 76 | ROOT_URLCONF = 'project.urls' 77 | 78 | TEMPLATES = [ 79 | { 80 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 81 | 'DIRS': [ 82 | os.path.join(PROJECT_DIR, 'templates'), 83 | ], 84 | 'APP_DIRS': True, 85 | 'OPTIONS': { 86 | 'context_processors': [ 87 | 'django.template.context_processors.debug', 88 | 'django.template.context_processors.request', 89 | 'django.contrib.auth.context_processors.auth', 90 | 'django.contrib.messages.context_processors.messages', 91 | ], 92 | }, 93 | }, 94 | ] 95 | 96 | WSGI_APPLICATION = 'project.wsgi.application' 97 | 98 | 99 | # Database 100 | # https://docs.djangoproject.com/en/2.1/ref/settings/#databases 101 | DATABASES = { 102 | 'default': { 103 | 'ENGINE': 'django.db.backends.sqlite3', 104 | 'NAME': os.path.join(os.path.join(os.path.dirname(__file__), '..', '..'), 'db.sqlite3'), 105 | } 106 | } 107 | 108 | 109 | # Password validation 110 | # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators 111 | 112 | AUTH_PASSWORD_VALIDATORS = [ 113 | { 114 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 115 | }, 116 | { 117 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 118 | }, 119 | { 120 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 121 | }, 122 | { 123 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 124 | }, 125 | ] 126 | 127 | 128 | # Internationalization 129 | # https://docs.djangoproject.com/en/2.1/topics/i18n/ 130 | 131 | LANGUAGE_CODE = 'en-us' 132 | 133 | TIME_ZONE = 'UTC' 134 | 135 | USE_I18N = True 136 | 137 | USE_L10N = True 138 | 139 | USE_TZ = True 140 | 141 | 142 | # Static files (CSS, JavaScript, Images) 143 | # https://docs.djangoproject.com/en/2.1/howto/static-files/ 144 | 145 | STATICFILES_FINDERS = [ 146 | 'django.contrib.staticfiles.finders.FileSystemFinder', 147 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 148 | ] 149 | 150 | STATICFILES_DIRS = [ 151 | os.path.join(PROJECT_DIR, 'static'), 152 | ] 153 | 154 | # ManifestStaticFilesStorage is recommended in production, to prevent outdated 155 | # Javascript / CSS assets being served from cache (e.g. after a Wagtail upgrade). 156 | # See https://docs.djangoproject.com/en/2.1/ref/contrib/staticfiles/#manifeststaticfilesstorage 157 | STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage' 158 | 159 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 160 | STATIC_URL = '/static/' 161 | 162 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 163 | MEDIA_URL = '/media/' 164 | 165 | 166 | # Wagtail settings 167 | 168 | WAGTAIL_SITE_NAME = "project" 169 | 170 | # Base URL to use when referring to full URLs within the Wagtail admin backend - 171 | # e.g. in notification emails. Don't include '/admin' or a trailing slash 172 | BASE_URL = 'http://example.com' 173 | 174 | 175 | GRAPHENE = { 176 | 'SCHEMA': 'wagtail_graphql.schema.schema', 177 | } 178 | 179 | GRAPHQL_API = { 180 | 'APPS': [ 181 | 'test_app_1', 182 | 'test_app_2', 183 | ], 184 | 'PREFIX': { 185 | }, 186 | 'URL_PREFIX': { 187 | 188 | }, 189 | 'RELAY': False, 190 | } 191 | -------------------------------------------------------------------------------- /tests/test_project/project/settings/relay.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | 3 | # SECURITY WARNING: don't run with debug turned on in production! 4 | DEBUG = True 5 | CORS_ORIGIN_ALLOW_ALL = DEBUG 6 | 7 | # SECURITY WARNING: keep the secret key used in production secret! 8 | SECRET_KEY = 'l_0ybxo5&rg-b&5-ya$a5v-kks7&znfl)b52@n9@5qt@to$b%j' 9 | 10 | # SECURITY WARNING: define the correct hosts in production! 11 | ALLOWED_HOSTS = ['*'] 12 | 13 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 14 | 15 | GRAPHQL_API['RELAY'] = True 16 | -------------------------------------------------------------------------------- /tests/test_project/project/settings/test.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | 3 | # SECURITY WARNING: don't run with debug turned on in production! 4 | DEBUG = True 5 | CORS_ORIGIN_ALLOW_ALL = DEBUG 6 | 7 | # SECURITY WARNING: keep the secret key used in production secret! 8 | SECRET_KEY = 'l_0ybxo5&rg-b&5-ya$a5v-kks7&znfl)b52@n9@5qt@to$b%j' 9 | 10 | # SECURITY WARNING: define the correct hosts in production! 11 | ALLOWED_HOSTS = ['*'] 12 | 13 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/test_project/project/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls import include, url 3 | from django.contrib import admin 4 | 5 | from wagtail.admin import urls as wagtailadmin_urls 6 | from wagtail.core import urls as wagtail_urls 7 | from wagtail.documents import urls as wagtaildocs_urls 8 | 9 | from django.views.decorators.csrf import csrf_exempt 10 | from graphene_django.views import GraphQLView 11 | from wagtail.images.views.serve import ServeView 12 | 13 | 14 | class CatchErrorsMiddleware(object): 15 | 16 | def on_error(self, error): 17 | import traceback 18 | traceback.print_tb(error.__traceback__) 19 | raise error 20 | 21 | def resolve(self, next, root, info, **args): 22 | return next(root, info, **args).catch(self.on_error) 23 | 24 | 25 | 26 | urlpatterns = [ 27 | url(r'^django-admin/', admin.site.urls), 28 | url(r'^admin/', include(wagtailadmin_urls)), 29 | url(r'^documents/', include(wagtaildocs_urls)), 30 | url(r'^graphql', csrf_exempt(GraphQLView.as_view())), 31 | 32 | url(r'^graphql', csrf_exempt(GraphQLView.as_view())), 33 | url(r'^graphiql', csrf_exempt(GraphQLView.as_view(graphiql=True, pretty=True, middleware=[CatchErrorsMiddleware]))), 34 | 35 | url(r'^images/([^/]*)/(\d*)/([^/]*)/[^/]*$', ServeView.as_view(), name='wagtailimages_serve'), 36 | 37 | # For anything not caught by a more specific rule above, hand over to 38 | # Wagtail's page serving mechanism. This should be the last pattern in 39 | # the list: 40 | url(r'', include(wagtail_urls)), 41 | 42 | # Alternatively, if you want Wagtail pages to be served from a subpath 43 | # of your site, rather than the site root: 44 | # url(r'^pages/', include(wagtail_urls)), 45 | ] 46 | 47 | 48 | if settings.DEBUG: 49 | from django.conf.urls.static import static 50 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 51 | 52 | # Serve static and media files from development server 53 | urlpatterns += staticfiles_urlpatterns() 54 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 55 | -------------------------------------------------------------------------------- /tests/test_project/project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for project project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings.dev") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /tests/test_project/test_app_1/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tr11/wagtail-graphql/1c10172f2fb11a5568a8099de00ed2afb59340d7/tests/test_project/test_app_1/__init__.py -------------------------------------------------------------------------------- /tests/test_project/test_app_1/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [ 8 | ('wagtailcore', '0040_page_draft_title'), 9 | ] 10 | 11 | operations = [ 12 | migrations.CreateModel( 13 | name='HomePage', 14 | fields=[ 15 | ('page_ptr', models.OneToOneField(on_delete=models.CASCADE, parent_link=True, auto_created=True, primary_key=True, serialize=False, to='wagtailcore.Page')), 16 | ], 17 | options={ 18 | 'abstract': False, 19 | }, 20 | bases=('wagtailcore.page',), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /tests/test_project/test_app_1/migrations/0002_create_homepage.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.db import migrations 3 | 4 | 5 | def create_homepage(apps, schema_editor): 6 | # Get models 7 | ContentType = apps.get_model('contenttypes.ContentType') 8 | Page = apps.get_model('wagtailcore.Page') 9 | Site = apps.get_model('wagtailcore.Site') 10 | HomePage = apps.get_model('test_app_1.HomePage') 11 | 12 | # Delete the default homepage 13 | # If migration is run multiple times, it may have already been deleted 14 | Page.objects.filter(id=2).delete() 15 | 16 | # Create content type for homepage model 17 | homepage_content_type, __ = ContentType.objects.get_or_create( 18 | model='homepage', app_label='test_app_1') 19 | 20 | # Create a new homepage 21 | homepage = HomePage.objects.create( 22 | title="Home", 23 | draft_title="Home", 24 | slug='home', 25 | content_type=homepage_content_type, 26 | path='00010001', 27 | depth=2, 28 | numchild=0, 29 | url_path='/home/', 30 | ) 31 | 32 | # Create a site with the new homepage set as the root 33 | Site.objects.create( 34 | hostname='localhost', root_page=homepage, is_default_site=True) 35 | 36 | 37 | def remove_homepage(apps, schema_editor): 38 | # Get models 39 | ContentType = apps.get_model('contenttypes.ContentType') 40 | HomePage = apps.get_model('test_app_1.HomePage') 41 | 42 | # Delete the default homepage 43 | # Page and Site objects CASCADE 44 | HomePage.objects.filter(slug='home', depth=2).delete() 45 | 46 | # Delete content type for homepage model 47 | ContentType.objects.filter(model='homepage', app_label='test_app_1').delete() 48 | 49 | 50 | class Migration(migrations.Migration): 51 | 52 | dependencies = [ 53 | ('test_app_1', '0001_initial'), 54 | ] 55 | 56 | operations = [ 57 | migrations.RunPython(create_homepage, remove_homepage), 58 | ] 59 | -------------------------------------------------------------------------------- /tests/test_project/test_app_1/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tr11/wagtail-graphql/1c10172f2fb11a5568a8099de00ed2afb59340d7/tests/test_project/test_app_1/migrations/__init__.py -------------------------------------------------------------------------------- /tests/test_project/test_app_1/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from modelcluster.fields import ParentalKey 3 | from wagtail.admin.edit_handlers import FieldPanel, InlinePanel 4 | from wagtail.contrib.forms.models import AbstractForm, AbstractFormField 5 | 6 | from wagtail.core.models import Page 7 | from wagtail.snippets.models import register_snippet 8 | 9 | 10 | class HomePage(Page): 11 | field_char = models.CharField(max_length=10, null=True, blank=True) 12 | field_int = models.IntegerField(null=True, blank=True) 13 | field_bool = models.BooleanField(default=False) 14 | field_date = models.DateField(null=True, blank=True) 15 | field_datetime = models.DateTimeField(null=True, blank=True) 16 | field_url = models.URLField(null=True, blank=True) 17 | field_decimal = models.DecimalField(null=True, decimal_places=2, max_digits=5, blank=True) 18 | field_email = models.EmailField(null=True, blank=True) 19 | field_float = models.FloatField(null=True, blank=True) 20 | field_duration = models.DurationField(null=True, blank=True) 21 | field_intp = models.PositiveIntegerField(null=True, blank=True) 22 | field_smallintp = models.PositiveSmallIntegerField(null=True, blank=True) 23 | field_smallint = models.SmallIntegerField(null=True, blank=True) 24 | field_text = models.TextField(null=True, blank=True) 25 | field_time = models.TimeField(null=True, blank=True) 26 | field_ip = models.GenericIPAddressField(null=True, blank=True) 27 | field_uuid = models.UUIDField(null=True, blank=True) 28 | 29 | content_panels = Page.content_panels + [ 30 | FieldPanel('field_char'), 31 | FieldPanel('field_int'), 32 | FieldPanel('field_bool'), 33 | FieldPanel('field_date'), 34 | FieldPanel('field_datetime'), 35 | FieldPanel('field_url'), 36 | FieldPanel('field_decimal'), 37 | FieldPanel('field_email'), 38 | FieldPanel('field_float'), 39 | FieldPanel('field_duration'), 40 | FieldPanel('field_intp'), 41 | FieldPanel('field_smallintp'), 42 | FieldPanel('field_smallint'), 43 | FieldPanel('field_text'), 44 | FieldPanel('field_time'), 45 | FieldPanel('field_ip'), 46 | FieldPanel('field_uuid'), 47 | ] 48 | 49 | 50 | @register_snippet 51 | class Advert(models.Model): 52 | url = models.URLField(null=True, blank=True) 53 | text = models.CharField(max_length=255) 54 | 55 | panels = [ 56 | FieldPanel('url'), 57 | FieldPanel('text'), 58 | ] 59 | 60 | def __str__(self): 61 | return self.text 62 | 63 | 64 | class FormField(AbstractFormField): 65 | page = ParentalKey('FormPage', on_delete=models.CASCADE, related_name='form_fields') 66 | 67 | 68 | class FormPage(AbstractForm): 69 | body = models.TextField() 70 | thank_you_text = models.TextField(blank=True) 71 | 72 | content_panels = AbstractForm.content_panels + [ 73 | FieldPanel('body'), 74 | FieldPanel('thank_you_text', classname="full"), 75 | InlinePanel('form_fields', label="Form fields"), 76 | ] 77 | -------------------------------------------------------------------------------- /tests/test_project/test_app_1/templates/home/home_page.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block body_class %}template-homepage{% endblock %} 4 | 5 | {% block content %} 6 |

Welcome to your new Wagtail site!

7 | 8 |

You can access the admin interface here (make sure you have run "./manage.py createsuperuser" in the console first).

9 | 10 |

If you haven't already given the documentation a read, head over to http://docs.wagtail.io to start building on Wagtail

11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /tests/test_project/test_app_2/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tr11/wagtail-graphql/1c10172f2fb11a5568a8099de00ed2afb59340d7/tests/test_project/test_app_2/__init__.py -------------------------------------------------------------------------------- /tests/test_project/test_app_2/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /tests/test_project/test_app_2/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TestApp2Config(AppConfig): 5 | name = 'test_app_2' 6 | -------------------------------------------------------------------------------- /tests/test_project/test_app_2/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from wagtail.core.fields import StreamField 3 | from wagtail.core.models import Page 4 | from wagtail.core import blocks 5 | from wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanel 6 | from wagtail.images.blocks import ImageChooserBlock 7 | from wagtail.core.blocks import PageChooserBlock 8 | from wagtail.snippets.blocks import SnippetChooserBlock 9 | from wagtail.snippets.models import register_snippet 10 | from wagtail.contrib.settings.models import BaseSetting, register_setting 11 | 12 | 13 | @register_setting 14 | class SiteBranding(BaseSetting): 15 | site_setting1 = models.TextField() 16 | site_setting2 = models.TextField() 17 | 18 | panels = [ 19 | FieldPanel('site_setting1'), 20 | FieldPanel('site_setting2'), 21 | ] 22 | 23 | 24 | @register_setting 25 | class AnotherSetting(BaseSetting): 26 | name = models.TextField() 27 | setting = models.TextField() 28 | 29 | panels = [ 30 | FieldPanel('name'), 31 | FieldPanel('setting'), 32 | ] 33 | 34 | 35 | @register_snippet 36 | class App2Snippet(models.Model): 37 | text = models.CharField(max_length=255) 38 | 39 | panels = [ 40 | FieldPanel('text'), 41 | ] 42 | 43 | def __str__(self): 44 | return self.text 45 | 46 | 47 | class CustomBlockInner(blocks.StructBlock): 48 | field_text = blocks.TextBlock(required=False) 49 | 50 | 51 | class CustomBlock1(blocks.StructBlock): 52 | field_char = blocks.CharBlock(required=False) 53 | field_text = blocks.TextBlock(required=False) 54 | field_email = blocks.EmailBlock(required=False) 55 | field_int = blocks.IntegerBlock(required=False) 56 | field_float = blocks.FloatBlock(required=False) 57 | field_decimal = blocks.DecimalBlock(required=False) 58 | field_regex = blocks.RegexBlock(regex=r'^[0-9]{3}$', required=False) 59 | field_url = blocks.URLBlock(required=False) 60 | field_bool = blocks.BooleanBlock(required=False) 61 | field_date = blocks.DateBlock(required=False) 62 | field_time = blocks.TimeBlock(required=False) 63 | field_datetime = blocks.DateTimeBlock(required=False) 64 | field_rich = blocks.RichTextBlock(required=False) 65 | field_raw = blocks.RawHTMLBlock(required=False) 66 | field_quote = blocks.BlockQuoteBlock(required=False) 67 | field_choice = blocks.ChoiceBlock(choices=[('tea', 'Tea'), ('coffee', 'Coffee')], icon='cup', required=False) 68 | field_static = blocks.StaticBlock(required=False) 69 | field_list = blocks.ListBlock(blocks.CharBlock, required=False) 70 | field_list_2 = blocks.ListBlock(CustomBlockInner, required=False) 71 | 72 | 73 | class CustomBlock2(blocks.StructBlock): 74 | field_link = PageChooserBlock(required=False) 75 | field_link_list = blocks.ListBlock(PageChooserBlock(), required=False) 76 | field_image = ImageChooserBlock(required=False) 77 | field_image_list = blocks.ListBlock(ImageChooserBlock(), required=False) 78 | field_snippet = SnippetChooserBlock(target_model=App2Snippet, required=False) 79 | field_snippet_list = blocks.ListBlock(SnippetChooserBlock(target_model=App2Snippet), required=False) 80 | 81 | 82 | class PageTypeA(Page): 83 | streamfield = StreamField([ 84 | ('h1', blocks.CharBlock(icon="title", classname="title")), 85 | ('h2', blocks.CharBlock(icon="subtitle", classname="subtitle")), 86 | ('n1', blocks.IntegerBlock(icon="subtitle", classname="subtitle")), 87 | ], null=True, blank=True) 88 | 89 | another = StreamField([ 90 | ('h3', blocks.CharBlock(icon="title", classname="title")), 91 | ('h4', blocks.CharBlock(icon="subtitle", classname="subtitle")), 92 | ('n2', blocks.IntegerBlock(icon="subtitle", classname="subtitle")), 93 | ], null=True, blank=True) 94 | 95 | third = StreamField([ 96 | ('char', blocks.CharBlock()), 97 | ('text', blocks.TextBlock()), 98 | ('email', blocks.EmailBlock()), 99 | ('int', blocks.IntegerBlock()), 100 | ('float', blocks.FloatBlock()), 101 | ('decimal', blocks.DecimalBlock()), 102 | ('regex', blocks.RegexBlock(regex=r'^[0-9]{3}$')), 103 | ('url', blocks.URLBlock()), 104 | ('bool', blocks.BooleanBlock()), 105 | ('date', blocks.DateBlock()), 106 | ('time', blocks.TimeBlock()), 107 | ('datetime', blocks.DateTimeBlock()), 108 | ('rich', blocks.RichTextBlock()), 109 | ('raw', blocks.RawHTMLBlock()), 110 | ('quote', blocks.BlockQuoteBlock()), 111 | ('choice', blocks.ChoiceBlock(choices=[('tea', 'Tea'), ('coffee', 'Coffee')], icon='cup')), 112 | ('static', blocks.StaticBlock()), 113 | ], null=True) 114 | 115 | links = StreamField([ 116 | ('image', ImageChooserBlock()), 117 | ('page', PageChooserBlock()), 118 | ('snippet', SnippetChooserBlock(target_model=App2Snippet)), 119 | ], null=True) 120 | 121 | lists = StreamField([ 122 | ('char', blocks.ListBlock(blocks.CharBlock())), 123 | ('text', blocks.ListBlock(blocks.TextBlock())), 124 | ('int', blocks.ListBlock(blocks.IntegerBlock())), 125 | ('float', blocks.ListBlock(blocks.FloatBlock())), 126 | ('decimal', blocks.ListBlock(blocks.DecimalBlock())), 127 | ('date', blocks.ListBlock(blocks.DateBlock())), 128 | ('time', blocks.ListBlock(blocks.TimeBlock())), 129 | ('datetime', blocks.ListBlock(blocks.DateTimeBlock())), 130 | ], null=True) 131 | 132 | links_list = StreamField([ 133 | ('image', blocks.ListBlock(ImageChooserBlock())), 134 | ('page', blocks.ListBlock(PageChooserBlock())), 135 | ('snippet', blocks.ListBlock(SnippetChooserBlock(target_model=App2Snippet))), 136 | ], null=True) 137 | 138 | custom = StreamField([ 139 | ('custom1', CustomBlock1()), 140 | ('custom2', CustomBlock2()), 141 | ], null=True) 142 | 143 | another_custom = StreamField([ 144 | ('custom1', CustomBlock1()), 145 | ('custom2', CustomBlock2()), 146 | ], null=True) 147 | 148 | custom_lists = StreamField([ 149 | ('custom1', blocks.ListBlock(CustomBlock1())), 150 | ('custom2', blocks.ListBlock(CustomBlock2())), 151 | ], null=True) 152 | 153 | content_panels = [ 154 | FieldPanel('title', classname="full title"), 155 | StreamFieldPanel('streamfield'), 156 | StreamFieldPanel('another'), 157 | StreamFieldPanel('third'), 158 | StreamFieldPanel('links'), 159 | StreamFieldPanel('custom'), 160 | StreamFieldPanel('another_custom'), 161 | StreamFieldPanel('lists'), 162 | StreamFieldPanel('links_list'), 163 | StreamFieldPanel('custom_lists'), 164 | ] 165 | 166 | 167 | -------------------------------------------------------------------------------- /tests/test_project/test_app_2/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /tests/test_project/test_app_2/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /tests/test_registry.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.django_db 5 | def test_reverse_models(client): 6 | from wagtail_graphql.registry import registry 7 | 8 | # force loading the api 9 | client.post('/graphql', {"query": "{ format }"}) 10 | 11 | models = registry.models 12 | assert {v: k for k, v in models.items()} == registry.rmodels 13 | 14 | 15 | @pytest.mark.django_db 16 | def test_reverse_snippets(client): 17 | from wagtail_graphql.registry import registry 18 | 19 | # force loading the api 20 | client.post('/graphql', {"query": "{ format }"}) 21 | 22 | snippets = registry.snippets 23 | assert {v: k for k, v in snippets.items()} == registry.rsnippets 24 | -------------------------------------------------------------------------------- /tests/test_settings.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from .graphql import assert_query, assert_query_fail 3 | from django.conf import settings 4 | IS_RELAY = settings.GRAPHQL_API.get('RELAY', False) 5 | 6 | 7 | @pytest.mark.skipif(IS_RELAY, reason="requires the relay setting") 8 | @pytest.mark.django_db 9 | def test_settings(client): 10 | assert_query(client, 'settings') 11 | 12 | 13 | @pytest.mark.skipif(not IS_RELAY, reason="requires the relay setting to be off") 14 | @pytest.mark.django_db 15 | def test_settings_relay(client): 16 | assert_query(client, 'settings', 'relay') 17 | 18 | 19 | @pytest.mark.skipif(IS_RELAY, reason="requires the relay setting") 20 | @pytest.mark.django_db 21 | def test_settings_fail(client): 22 | assert_query_fail(client, 'settings', 'fail') 23 | -------------------------------------------------------------------------------- /tests/test_site.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from .graphql import assert_query 3 | 4 | 5 | @pytest.mark.django_db 6 | def test_site(client): 7 | client.post('/graphql', 8 | {"query": 'mutation { login(username: "admin", password: "password") { user { username } } }'} 9 | ) 10 | assert_query(client, 'site') 11 | 12 | 13 | @pytest.mark.django_db 14 | def test_site_fail(client): 15 | response = client.post('/graphql', {"query": 'query {root { id } }'}) 16 | assert response.status_code == 200 17 | assert {'data': {'root': None}} == response.json() 18 | -------------------------------------------------------------------------------- /tests/test_snippets.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from .graphql import assert_query 3 | from django.conf import settings 4 | IS_RELAY = settings.GRAPHQL_API.get('RELAY', False) 5 | 6 | 7 | @pytest.mark.skipif(IS_RELAY, reason="requires the relay setting") 8 | @pytest.mark.django_db 9 | def test_snippets(client): 10 | assert_query(client, 'snippets', '1') 11 | 12 | 13 | @pytest.mark.skipif(not IS_RELAY, reason="requires the relay setting to be off") 14 | @pytest.mark.django_db 15 | def test_snippets_relay(client): 16 | assert_query(client, 'snippets', '1', 'relay') 17 | -------------------------------------------------------------------------------- /tests/test_streamfield.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from .graphql import assert_query 3 | from django.conf import settings 4 | IS_RELAY = settings.GRAPHQL_API.get('RELAY', False) 5 | 6 | 7 | @pytest.mark.skipif(IS_RELAY, reason="requires the relay setting") 8 | @pytest.mark.django_db 9 | def test_streamfield_scalar_blocks(client): 10 | assert_query(client, 'test_app_2', 'streamfield') 11 | 12 | 13 | @pytest.mark.skipif(not IS_RELAY, reason="requires the relay setting to be off") 14 | @pytest.mark.django_db 15 | def test_streamfield_scalar_blocks_relay(client): 16 | assert_query(client, 'test_app_2', 'streamfield', 'relay') 17 | -------------------------------------------------------------------------------- /tests/test_wagtail_graphql.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from wagtail_graphql import __version__ 3 | from django.conf import settings 4 | IS_RELAY = settings.GRAPHQL_API.get('RELAY', False) 5 | 6 | EXPECTED_VERSION = '0.2.0' 7 | EXPECTED_API_VERSION = '0.2.0' 8 | 9 | 10 | def test_version_import(): 11 | assert __version__ == EXPECTED_VERSION 12 | 13 | 14 | @pytest.mark.django_db 15 | def test_version_api(client): 16 | response = client.post('/graphql', {"query": "{ format }"}) 17 | assert response.status_code == 200 18 | assert response.json() == {'data': {'format': EXPECTED_API_VERSION}} 19 | 20 | 21 | @pytest.mark.django_db 22 | def test_types(client): 23 | response = client.post('/graphql', {"query": "{__schema { types { name } } }"}) 24 | assert response.status_code == 200 25 | expected = { 26 | 'Query', 27 | 'Int', 28 | 'String', 29 | 'Boolean', 30 | 'User', 31 | 'ID', 32 | 'DateTime', 33 | 'Document', 34 | 'Image', 35 | 'Rect', 36 | 'Site', 37 | 'Test_app_1HomePage', 38 | 'Mutation', 39 | '__Schema', 40 | '__Type', 41 | '__TypeKind', 42 | '__Field', 43 | '__InputValue', 44 | '__EnumValue', 45 | '__Directive', 46 | '__DirectiveLocation', 47 | 'Date', 48 | 'Float', 49 | 'BasePage', 50 | 'Page', 51 | 'LoginMutation', 52 | 'LogoutMutation', 53 | 'Test_app_2PageTypeAAnotherType', 54 | 'Test_app_2PageTypeA', 55 | 'StringBlock', 56 | 'Test_app_2PageTypeAStreamfieldType', 57 | 'IntBlock', 58 | 'Time', 59 | 'GenericScalar', 60 | 'Test_app_2PageTypeAThirdType', 61 | 'DateBlock', 62 | 'GenericScalarBlock', 63 | 'BooleanBlock', 64 | 'UUID', 65 | 'FloatBlock', 66 | 'DateTimeBlock', 67 | 'TimeBlock', 68 | 'Snippet', 69 | 'Test_app_1Advert', 70 | 'MainMenuMaxLevels', 71 | 'FlatMenuMaxLevels', 72 | 'SecondaryMenuItem', 73 | 'MenuItem', 74 | 'Menu', 75 | 'SecondaryMenu', 76 | 'Test_app_2PageTypeAAnotherCustomType', 77 | 'Test_app_2PageTypeACustomType', 78 | 'PageBlock', 79 | 'Test_app_2CustomBlockInner', 80 | 'Test_app_2PageTypeALinksType', 81 | 'Test_app_2CustomBlock1', 82 | 'Test_app_2CustomBlock2', 83 | 'Test_app_2App2Snippet', 84 | 'Test_app_2App2SnippetBlock', 85 | 'ImageBlock', 86 | 'DateTimeListBlock', 87 | 'Test_app_2App2SnippetListBlock', 88 | 'Test_app_2PageTypeALinksListType', 89 | 'Test_app_2PageTypeAListsType', 90 | 'Test_app_2CustomBlock2ListBlock', 91 | 'Test_app_2CustomBlock1ListBlock', 92 | 'FloatListBlock', 93 | 'TimeListBlock', 94 | 'DateListBlock', 95 | 'PageListBlock', 96 | 'ImageListBlock', 97 | 'Test_app_2PageTypeACustomListsType', 98 | 'IntListBlock', 99 | 'StringListBlock', 100 | 'DateTimeListBlock', 101 | 'Test_app_2App2SnippetListBlock', 102 | 'Settings', 103 | 'Test_app_2SiteBranding', 104 | 'FormFieldFieldType', 105 | 'FormField', 106 | 'FormError', 107 | 'Test_app_1FormPage', 108 | 'Test_app_1FormField', 109 | 'Test_app_1FormPageMutation', 110 | 'Test_app_2AnotherSetting', 111 | } 112 | if IS_RELAY: 113 | expected.update([ 114 | 'PageEdge', 115 | 'Node', 116 | 'PageConnection', 117 | 'PageInfo', 118 | 'BasePageEdge', 119 | 'BasePageConnection' 120 | ]) 121 | 122 | assert set(x['name'] for x in response.json()['data']['__schema']['types']) == expected 123 | -------------------------------------------------------------------------------- /wagtail_graphql/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | __version__ = '0.2.0' 3 | -------------------------------------------------------------------------------- /wagtail_graphql/actions.py: -------------------------------------------------------------------------------- 1 | # python 2 | import string 3 | from typing import Type, Set 4 | # django 5 | from django.utils.text import camel_case_to_spaces 6 | # graphene 7 | import graphene 8 | from graphene.types.generic import GenericScalar 9 | # graphene_django 10 | from graphene_django import DjangoObjectType 11 | # wagtail 12 | from wagtail.core.fields import StreamField 13 | from wagtail.core.models import Page as wagtailPage 14 | # wagtail forms 15 | from wagtail.contrib.forms.models import AbstractForm 16 | # wagtail settings 17 | from wagtail.contrib.settings.models import BaseSetting 18 | # app 19 | from .registry import registry 20 | from .permissions import with_page_permissions 21 | from .settings import url_prefix_for_site, RELAY 22 | # app types 23 | from .types import ( 24 | Page, 25 | Settings, 26 | FormError, 27 | FormField, 28 | ) 29 | 30 | 31 | def _add_form(cls: Type[AbstractForm], node: str, dict_params: dict) -> Type[graphene.Mutation]: 32 | if node in registry.forms: # pragma: no cover 33 | return registry.forms[node] 34 | 35 | registry.page_prefetch_fields.add(cls.__name__.lower()) 36 | dict_params['Meta'].interfaces += (Page,) 37 | dict_params['form_fields'] = graphene.List(FormField) 38 | 39 | def form_fields(self, _info): 40 | return list(FormField(name=field_.clean_name, field_type=field_.field_type, 41 | label=field_.label, required=field_.required, 42 | help_text=field_.help_text, choices=field_.choices, 43 | default_value=field_.default_value) 44 | for field_ in self.form_fields.all()) 45 | 46 | dict_params['resolve_form_fields'] = form_fields 47 | registry.pages[cls] = type(node, (DjangoObjectType,), dict_params) 48 | 49 | args = type("Arguments", (), {'values': GenericScalar(), 50 | "url": graphene.String(required=True)}) 51 | _node = node 52 | 53 | def mutate(_self, info, url, values): 54 | url_prefix = url_prefix_for_site(info) 55 | query = wagtailPage.objects.filter(url_path=url_prefix + url.rstrip('/') + '/') 56 | instance = with_page_permissions( 57 | info.context, 58 | query.specific() 59 | ).live().first() 60 | user = info.context.user 61 | # convert camelcase to dashes 62 | values = {camel_case_to_spaces(k).replace(' ', '-'): v for k, v in values.items()} 63 | form = instance.get_form(values, None, page=instance, user=user) 64 | if form.is_valid(): 65 | # form_submission 66 | instance.process_form_submission(form) 67 | return registry.forms[_node](result="OK") 68 | else: 69 | return registry.forms[_node](result="FAIL", errors=[FormError(*err) for err in form.errors.items()]) 70 | 71 | dict_params = { 72 | "Arguments": args, 73 | "mutate": mutate, 74 | "result": graphene.String(), 75 | "errors": graphene.List(FormError), 76 | } 77 | tp = type(node + "Mutation", (graphene.Mutation,), dict_params) # type: Type[graphene.Mutation] 78 | registry.forms[node] = tp 79 | return tp 80 | 81 | 82 | def _add_page(cls: Type[wagtailPage], node: str, dict_params: dict) -> Type[DjangoObjectType]: 83 | if cls in registry.pages: # pragma: no cover 84 | return registry.pages[cls] 85 | registry.page_prefetch_fields.add(cls.__name__.lower()) 86 | dict_params['Meta'].interfaces += (Page,) 87 | tp = type(node, (DjangoObjectType,), dict_params) # type: Type[DjangoObjectType] 88 | registry.pages[cls] = tp 89 | return tp 90 | 91 | 92 | def _add_setting(cls: Type[BaseSetting], node: str, dict_params: dict) -> Type[DjangoObjectType]: 93 | if not hasattr(cls, 'name'): # we always need a name field 94 | cls.name = cls.__name__ 95 | dict_params['Meta'].interfaces += (Settings,) 96 | tp = type(node, (DjangoObjectType,), dict_params) # type: Type[DjangoObjectType] 97 | registry.settings[node] = (tp, cls) 98 | return tp 99 | 100 | 101 | def _add_snippet(cls: type, node: str, dict_params: dict) -> Type[DjangoObjectType]: 102 | if cls in registry.snippets: # pragma: no cover 103 | return registry.snippets[cls] 104 | tp = type(node, (DjangoObjectType,), dict_params) # type: Type[DjangoObjectType] 105 | registry.snippets[cls] = tp 106 | registry.snippets_by_name[node] = tp 107 | return tp 108 | 109 | 110 | def _add_django_model(_cls: type, node: str, dict_params: dict) -> Type[DjangoObjectType]: 111 | if node in registry.django: # pragma: no cover 112 | return registry.django[node] 113 | tp = type(node, (DjangoObjectType,), dict_params) # type: Type[DjangoObjectType] 114 | registry.django[node] = tp 115 | return tp 116 | 117 | 118 | def _add_streamfields(cls: wagtailPage, node: str, dict_params: dict, app: str, prefix: str) -> None: 119 | from .types.streamfield import ( 120 | block_handler, 121 | stream_field_handler, 122 | ) 123 | 124 | for field in cls._meta.fields: 125 | if isinstance(field, StreamField): 126 | field_name = field.name 127 | stream_field_name = f"{node}{string.capwords(field_name, sep='_').replace('_', '')}" 128 | blocks = field.stream_block.child_blocks 129 | 130 | handlers = dict( 131 | (name, block_handler(block, app, prefix)) 132 | for name, block in blocks.items() 133 | ) 134 | 135 | f, resolve = stream_field_handler( 136 | stream_field_name, 137 | field_name, 138 | handlers 139 | ) 140 | 141 | dict_params.update({ 142 | field.name: f, 143 | "resolve_" + field.name: resolve 144 | }) 145 | 146 | 147 | def _register_model(registered: Set[type], cls: type, snippet: bool, 148 | app: str, prefix: str, override_name=None) -> None: 149 | if cls in registered: 150 | return 151 | 152 | prefix = prefix.format(app=string.capwords(app), 153 | cls=cls.__name__) 154 | node = override_name or prefix + cls.__name__ 155 | 156 | # dict parameters to create GraphQL type 157 | class Meta: 158 | model = cls 159 | interfaces = (graphene.relay.Node, ) if RELAY else tuple() 160 | 161 | dict_params = {'Meta': Meta} 162 | 163 | # add streamfield handlers 164 | _add_streamfields(cls, node, dict_params, app, prefix) 165 | 166 | if snippet: 167 | _add_snippet(cls, node, dict_params) 168 | elif issubclass(cls, AbstractForm): 169 | _add_form(cls, node, dict_params) 170 | elif issubclass(cls, wagtailPage): 171 | _add_page(cls, node, dict_params) 172 | elif issubclass(cls, BaseSetting): 173 | _add_setting(cls, node, dict_params) 174 | else: # Django Model 175 | _add_django_model(cls, node, dict_params) 176 | 177 | registered.add(cls) 178 | 179 | 180 | def add_app(app: str, prefix: str = '{app}') -> None: 181 | from django.contrib.contenttypes.models import ContentType 182 | from wagtail.snippets.models import get_snippet_models 183 | snippets = get_snippet_models() 184 | models = [mdl.model_class() 185 | for mdl in ContentType.objects.filter(app_label=app).all()] 186 | snippets = [s for s in snippets if s in models] 187 | to_register = [x for x in snippets + models if x is not None] 188 | registered: Set = set() 189 | 190 | # prefetch content_types 191 | ContentType.objects.get_for_models(*to_register) 192 | 193 | for cls in to_register: 194 | _register_model(registered, cls, cls in snippets, app, prefix) 195 | 196 | 197 | def add_apps_with_settings(settings: dict) -> None: 198 | apps = settings.get('APPS', []) 199 | 200 | for app in apps: 201 | prefixes = settings.get('PREFIX', {}) 202 | if isinstance(prefixes, str): 203 | prefix = prefixes # pragma: no cover 204 | else: 205 | prefix = prefixes.get(app, '{app}') 206 | add_app(app, prefix=prefix) 207 | if not apps: # pragma: no cover 208 | import logging 209 | logging.warning("No APPS specified for wagtail_graphql") 210 | 211 | 212 | def add_apps() -> None: 213 | from .settings import SETTINGS 214 | add_apps_with_settings(SETTINGS) 215 | 216 | 217 | # standard page 218 | _register_model(set(), wagtailPage, False, 'wagtailcore', '', override_name='BasePage') 219 | -------------------------------------------------------------------------------- /wagtail_graphql/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig # pragma: no cover 2 | 3 | 4 | class ApiConfig(AppConfig): # pragma: no cover 5 | name = 'wagtail_graphql' 6 | -------------------------------------------------------------------------------- /wagtail_graphql/permissions.py: -------------------------------------------------------------------------------- 1 | # python 2 | from typing import Any, Union 3 | # django 4 | from django.db.models import Q 5 | from django.contrib.auth.models import AnonymousUser 6 | # wagtail 7 | from wagtail.core.query import PageQuerySet 8 | from wagtail.core.models import PageViewRestriction, CollectionViewRestriction 9 | from wagtail.images.models import ImageQuerySet 10 | from wagtail.documents.models import DocumentQuerySet 11 | 12 | 13 | def with_page_permissions(request: Any, queryset: PageQuerySet) -> PageQuerySet: 14 | user = request.user 15 | 16 | # Filter by site 17 | if request.site: 18 | queryset = queryset.descendant_of(request.site.root_page, inclusive=True) 19 | else: 20 | # No sites configured 21 | return queryset.none() # pragma: no cover 22 | 23 | # Get live pages that are public and check groups and login permissions 24 | if user == AnonymousUser: 25 | queryset = queryset.public() 26 | elif user.is_superuser: 27 | pass 28 | else: 29 | current_user_groups = user.groups.all() 30 | q = Q() 31 | for restriction in PageViewRestriction.objects.all(): 32 | if (restriction.restriction_type == PageViewRestriction.PASSWORD) or \ 33 | (restriction.restriction_type == PageViewRestriction.LOGIN and not user.is_authenticated) or \ 34 | (restriction.restriction_type == PageViewRestriction.GROUPS and 35 | not any(group in current_user_groups for group in restriction.groups.all()) 36 | ): 37 | q = ~queryset.descendant_of_q(restriction.page, inclusive=True) 38 | queryset = queryset.filter(q).live() 39 | 40 | return queryset 41 | 42 | 43 | CollectionQSType = Union[ImageQuerySet, DocumentQuerySet] 44 | 45 | 46 | def with_collection_permissions(request: Any, queryset: CollectionQSType) -> CollectionQSType: 47 | user = request.user 48 | 49 | # Get live pages that are public and check groups and login permissions 50 | if user == AnonymousUser: 51 | queryset = queryset.public() 52 | elif user.is_superuser: 53 | pass 54 | else: 55 | current_user_groups = user.groups.all() 56 | q = Q() 57 | for restriction in CollectionViewRestriction.objects.all(): 58 | if (restriction.restriction_type == CollectionViewRestriction.PASSWORD) or \ 59 | (restriction.restriction_type == CollectionViewRestriction.LOGIN and not user.is_authenticated) or \ 60 | (restriction.restriction_type == CollectionViewRestriction.GROUPS and 61 | not any(group in current_user_groups for group in restriction.groups.all()) 62 | ): 63 | q &= ~Q(collection=restriction.collection) 64 | # q &= ~queryset.filter(collection) descendant_of_q(restriction.page, inclusive=True) 65 | queryset = queryset.filter(q) 66 | 67 | return queryset 68 | -------------------------------------------------------------------------------- /wagtail_graphql/registry.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class RegistryItem(dict): 4 | @property 5 | def types(self) -> tuple: 6 | return tuple(self.values()) 7 | 8 | 9 | class Registry: 10 | _django = RegistryItem() 11 | _forms = RegistryItem() 12 | _pages = RegistryItem() 13 | _settings = RegistryItem() 14 | _snippets = RegistryItem() 15 | _snippets_by_name = RegistryItem() 16 | _streamfield_blocks = RegistryItem() 17 | _streamfield_scalar_blocks = RegistryItem() 18 | _page_prefetch = { 19 | 'content_type', 'owner', 20 | 'live_revision', 'page_ptr' 21 | } 22 | 23 | @property 24 | def blocks(self) -> RegistryItem: 25 | return self._streamfield_blocks 26 | 27 | @property 28 | def scalar_blocks(self) -> RegistryItem: 29 | return self._streamfield_scalar_blocks 30 | 31 | @property 32 | def django(self) -> RegistryItem: 33 | return self._django 34 | 35 | @property 36 | def forms(self) -> RegistryItem: 37 | return self._forms 38 | 39 | @property 40 | def pages(self) -> RegistryItem: 41 | return self._pages 42 | 43 | @property 44 | def settings(self) -> RegistryItem: 45 | return self._settings 46 | 47 | @property 48 | def snippets(self) -> RegistryItem: 49 | return self._snippets 50 | 51 | @property 52 | def snippets_by_name(self) -> RegistryItem: 53 | return self._snippets_by_name 54 | 55 | @property 56 | def rsnippets(self) -> RegistryItem: 57 | return RegistryItem((v, k) for k, v in self._snippets.items()) 58 | 59 | @property 60 | def page_prefetch_fields(self) -> set: 61 | return self._page_prefetch 62 | 63 | @property 64 | def models(self) -> dict: 65 | models: dict = {} 66 | models.update(self.pages) 67 | models.update(self.snippets) 68 | models.update(self.forms) 69 | models.update(self.django) 70 | models.update((k, v[0]) for k, v in self.settings.items()) 71 | models.update((k, v) for k, v in self.blocks.items() if not isinstance(v, tuple)) 72 | models.update(self.scalar_blocks.items()) 73 | return models 74 | 75 | @property 76 | def rmodels(self) -> dict: 77 | return dict((v, k) for k, v in self.models.items()) 78 | 79 | 80 | registry = Registry() 81 | -------------------------------------------------------------------------------- /wagtail_graphql/relay.py: -------------------------------------------------------------------------------- 1 | # graphene 2 | import graphene 3 | # app 4 | from .settings import RELAY 5 | 6 | 7 | if RELAY: 8 | class RelayMixin: 9 | node = graphene.relay.Node.Field() 10 | else: 11 | class RelayMixin: # type: ignore 12 | pass 13 | -------------------------------------------------------------------------------- /wagtail_graphql/schema.py: -------------------------------------------------------------------------------- 1 | # typings 2 | from typing import Any # noqa 3 | # django 4 | from django.utils.text import camel_case_to_spaces 5 | # graphql 6 | from graphql import ResolveInfo 7 | # graphene 8 | import graphene 9 | # graphene_django 10 | from graphene_django.converter import String 11 | # app 12 | from .relay import RelayMixin 13 | from .registry import registry 14 | from .actions import add_apps 15 | # add all the apps from the settings 16 | add_apps() 17 | # mixins 18 | from .types import ( # noqa: E402 19 | AuthQueryMixin, LoginMutation, LogoutMutation, 20 | DocumentQueryMixin, 21 | ImageQueryMixin, 22 | InfoQueryMixin, 23 | MenusQueryMixin, 24 | PagesQueryMixin, 25 | SettingsQueryMixin, 26 | SnippetsQueryMixin, 27 | ) 28 | 29 | 30 | # api version 31 | GRAPHQL_API_FORMAT = (0, 2, 0) 32 | 33 | # mixins 34 | AuthQueryMixin_ = AuthQueryMixin() # type: Any 35 | DocumentQueryMixin_ = DocumentQueryMixin() # type: Any 36 | ImageQueryMixin_ = ImageQueryMixin() # type: Any 37 | InfoQueryMixin_ = InfoQueryMixin() # type: Any 38 | MenusQueryMixin_ = MenusQueryMixin() # type: Any 39 | PagesQueryMixin_ = PagesQueryMixin() # type: Any 40 | SettingsQueryMixin_ = SettingsQueryMixin() # type: Any 41 | SnippetsQueryMixin_ = SnippetsQueryMixin() # type: Any 42 | 43 | 44 | class Query(graphene.ObjectType, 45 | AuthQueryMixin_, 46 | DocumentQueryMixin_, 47 | ImageQueryMixin_, 48 | InfoQueryMixin_, 49 | MenusQueryMixin_, 50 | PagesQueryMixin_, 51 | SettingsQueryMixin_, 52 | SnippetsQueryMixin_, 53 | RelayMixin 54 | ): 55 | # API Version 56 | format = graphene.Field(String) 57 | 58 | def resolve_format(self, _info: ResolveInfo): 59 | return '%d.%d.%d' % GRAPHQL_API_FORMAT 60 | 61 | 62 | def mutation_parameters() -> dict: 63 | dict_params = { 64 | 'login': LoginMutation.Field(), 65 | 'logout': LogoutMutation.Field(), 66 | } 67 | dict_params.update((camel_case_to_spaces(n).replace(' ', '_'), mut.Field()) 68 | for n, mut in registry.forms.items()) 69 | return dict_params 70 | 71 | 72 | Mutations = type("Mutation", 73 | (graphene.ObjectType,), 74 | mutation_parameters() 75 | ) 76 | 77 | schema = graphene.Schema( 78 | query=Query, 79 | mutation=Mutations, 80 | types=list(registry.models.values()) 81 | ) 82 | -------------------------------------------------------------------------------- /wagtail_graphql/settings.py: -------------------------------------------------------------------------------- 1 | # django 2 | from django.conf import settings 3 | # graphql 4 | from graphql import ResolveInfo 5 | 6 | # settings 7 | if hasattr(settings, 'GRAPHQL_API'): 8 | SETTINGS = settings.GRAPHQL_API 9 | else: # pragma: no cover 10 | SETTINGS = {} 11 | URL_PREFIX = SETTINGS.get('URL_PREFIX', {}) 12 | LOAD_GENERIC_SCALARS = SETTINGS.get('GENERIC_SCALARS', True) 13 | RELAY = SETTINGS.get('RELAY', False) 14 | 15 | # wagtail settings 16 | try: 17 | from wagtail.contrib.settings.registry import registry as settings_registry 18 | except ImportError: # pragma: no cover 19 | settings_registry = None 20 | 21 | 22 | def url_prefix_for_site(info: ResolveInfo): 23 | hostname = info.context.site.hostname 24 | return URL_PREFIX.get( 25 | hostname, 26 | info.context.site.root_page.url_path.rstrip('/') 27 | ) 28 | -------------------------------------------------------------------------------- /wagtail_graphql/types/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import ( 2 | Page, 3 | Site, 4 | User, 5 | # mixins 6 | InfoQueryMixin, 7 | PagesQueryMixin, 8 | ) 9 | from .auth import ( 10 | LoginMutation, 11 | LogoutMutation, 12 | # mixins 13 | AuthQueryMixin, 14 | ) 15 | from .documents import ( 16 | Document, 17 | # mixins 18 | DocumentQueryMixin, 19 | ) 20 | from .forms import FormField, FormError 21 | from .images import ( 22 | Image, 23 | # mixins 24 | ImageQueryMixin, 25 | ) 26 | from .settings import Settings, SettingsQueryMixin 27 | from .snippets import SnippetsQueryMixin 28 | # noinspection PyUnresolvedReferences 29 | from . import converters # noqa: F401 30 | 31 | __all__ = [ 32 | # core 33 | 'Page', 34 | 'Settings', 35 | 'Site', 36 | 'User', 37 | # auth 38 | 'AuthQueryMixin', 39 | 'LoginMutation', 40 | 'LogoutMutation', 41 | # documents 42 | 'Document', 43 | # forms 44 | 'FormError', 45 | 'FormField', 46 | # images 47 | 'Image', 48 | # mixins 49 | 'DocumentQueryMixin', 50 | 'ImageQueryMixin', 51 | 'InfoQueryMixin', 52 | 'MenusQueryMixin', 53 | 'PagesQueryMixin', 54 | 'SettingsQueryMixin', 55 | 'SnippetsQueryMixin', 56 | ] 57 | 58 | # menus 59 | 60 | try: 61 | from django.conf import settings 62 | if 'wagtailmenus' not in settings.INSTALLED_APPS: 63 | raise ImportError() # pragma: no cover 64 | 65 | from .menus import MenusQueryMixin, Menu, MenuItem, SecondaryMenu, SecondaryMenuItem # noqa: F401 66 | 67 | __all__.extend([ 68 | # menus 69 | 'Menu', 70 | 'MenuItem', 71 | 'SecondaryMenu', 72 | 'SecondaryMenuItem', 73 | ]) 74 | 75 | HAS_WAGTAILMENUS = True 76 | except ImportError: # pragma: no cover 77 | def MenusQueryMixin(): 78 | # type: ignore 79 | class _MenusQueryMixin: 80 | pass 81 | return _MenusQueryMixin 82 | HAS_WAGTAILMENUS = False 83 | -------------------------------------------------------------------------------- /wagtail_graphql/types/auth.py: -------------------------------------------------------------------------------- 1 | # django 2 | from django.contrib.auth.models import User as wagtailUser, AnonymousUser 3 | # graphene 4 | import graphene 5 | from graphql.execution.base import ResolveInfo 6 | # app types 7 | from .core import User 8 | 9 | 10 | def AuthQueryMixin(): 11 | class Mixin: 12 | # User information 13 | user = graphene.Field(User) 14 | 15 | def resolve_user(self, info: ResolveInfo): 16 | user = info.context.user 17 | if isinstance(user, AnonymousUser): 18 | return wagtailUser(id='-1', username='anonymous') 19 | return user 20 | return Mixin 21 | 22 | 23 | class LoginMutation(graphene.Mutation): 24 | class Arguments: 25 | username = graphene.String(required=True) 26 | password = graphene.String(required=True) 27 | 28 | user = graphene.Field(User) 29 | 30 | def mutate(self, info, username, password): 31 | from django.contrib.auth import authenticate, login 32 | user = authenticate(info.context, username=username, password=password) 33 | if user is not None: 34 | login(info.context, user) 35 | else: 36 | user = wagtailUser(id='-1', username='anonymous') 37 | return LoginMutation(user=user) 38 | 39 | 40 | class LogoutMutation(graphene.Mutation): 41 | user = graphene.Field(User) 42 | 43 | def mutate(self, info): 44 | from django.contrib.auth import logout 45 | logout(info.context) 46 | return LogoutMutation(wagtailUser(id='-1', username='anonymous')) 47 | -------------------------------------------------------------------------------- /wagtail_graphql/types/converters.py: -------------------------------------------------------------------------------- 1 | # django 2 | from django.db import models 3 | # graphene_django 4 | from graphene import String 5 | from graphene_django.converter import convert_django_field 6 | 7 | 8 | @convert_django_field.register(models.DecimalField) 9 | @convert_django_field.register(models.DurationField) 10 | def convert_force_string(field, _registry=None): 11 | return String(description=field.help_text, required=not field.null) 12 | -------------------------------------------------------------------------------- /wagtail_graphql/types/core.py: -------------------------------------------------------------------------------- 1 | # typings 2 | from typing import cast 3 | # django 4 | from django.contrib.auth.models import User as wagtailUser 5 | from django.contrib.contenttypes.models import ContentType 6 | # graphql 7 | from graphql.execution.base import ResolveInfo 8 | from graphql.language.ast import InlineFragment 9 | # graphene 10 | import graphene 11 | # graphene_django 12 | from graphene_django import DjangoObjectType 13 | from graphene_django.converter import convert_django_field, String, List 14 | # wagtail 15 | from wagtail.core.models import Page as wagtailPage, Site as wagtailSite 16 | from taggit.managers import TaggableManager, 17 | from modelcluster.tags import ClusterTaggableManager 18 | from wagtail.core.utils import camelcase_to_underscore 19 | # app 20 | from ..settings import url_prefix_for_site, RELAY 21 | from ..registry import registry 22 | from ..permissions import with_page_permissions 23 | 24 | 25 | class User(DjangoObjectType): 26 | class Meta: 27 | model = wagtailUser 28 | exclude_fields = ['password'] 29 | 30 | 31 | class Site(DjangoObjectType): 32 | class Meta: 33 | model = wagtailSite 34 | 35 | 36 | interface_cls: graphene.Interface = graphene.relay.Node if RELAY else graphene.Interface 37 | 38 | 39 | class Page(interface_cls): 40 | if not RELAY: # use opaque ids in Relay 41 | id = graphene.Int(required=True) 42 | title = graphene.String(required=True) 43 | url_path = graphene.String() 44 | content_type = graphene.String() 45 | slug = graphene.String(required=True) 46 | path = graphene.String() 47 | depth = graphene.Int() 48 | seoTitle = graphene.String() 49 | numchild = graphene.Int() 50 | revision = graphene.Int() 51 | first_published_at = graphene.DateTime() 52 | last_published_at = graphene.DateTime() 53 | latest_revision_created_at = graphene.DateTime() 54 | live = graphene.Boolean() 55 | go_live_at = graphene.DateTime() 56 | expire_at = graphene.DateTime() 57 | expired = graphene.Boolean() 58 | locked = graphene.Boolean() 59 | draft_title = graphene.String() 60 | has_unpublished_changes = graphene.Boolean() 61 | 62 | def resolve_content_type(self, _info: ResolveInfo): 63 | self.content_type = ContentType.objects.get_for_model(self) 64 | return self.content_type.app_label + '.' + self.content_type.model_class().__name__ 65 | 66 | @classmethod 67 | def resolve_type(cls, instance, info: ResolveInfo) -> 'Page': 68 | mdl = ContentType.objects.get_for_model(instance).model_class() 69 | try: 70 | model = registry.pages[mdl] 71 | except KeyError: # pragma: no cover 72 | raise ValueError("Model %s is not a registered GraphQL type" % mdl) 73 | return model 74 | 75 | def resolve_url_path(self, info: ResolveInfo) -> str: 76 | self.url_path = cast(str, self.url_path) 77 | url_prefix = url_prefix_for_site(info) 78 | url = self.url_path if not self.url_path.startswith(url_prefix) else self.url_path[len(url_prefix):] 79 | return url.rstrip('/') 80 | 81 | if RELAY: 82 | children = graphene.ConnectionField(lambda *x: PageConnection) 83 | else: 84 | children = graphene.List(lambda *x: Page) 85 | 86 | def resolve_children(self, info: ResolveInfo, **_kwargs): 87 | query = wagtailPage.objects.child_of(self) 88 | return with_page_permissions( 89 | info.context, 90 | query.specific() 91 | ).live().order_by('path').all() 92 | 93 | 94 | # https://jossingram.wordpress.com/2018/04/19/wagtail-and-graphql/ 95 | class FlatTags(graphene.String): 96 | 97 | @classmethod 98 | def serialize(cls, value): 99 | tagsList = [] 100 | for tag in value.all(): 101 | tagsList.append(tag.name) 102 | return tagsList 103 | 104 | @convert_django_field.register(ClusterTaggableManager) 105 | def convert_tag_field_to_string(field, registry=None): 106 | return graphene.Field(FlatTags, 107 | description=field.help_text, 108 | required=not field.null) 109 | 110 | @convert_django_field.register(TaggableManager) 111 | def convert_field_to_string(field, _registry=None): 112 | return List(String, description=field.help_text, required=not field.null) 113 | 114 | 115 | def _resolve_preview(request, view): # pragma: no cover 116 | from django.http import QueryDict 117 | page = view.get_page() 118 | post_data, timestamp = request.session.get(view.session_key, (None, None)) 119 | if not isinstance(post_data, str): 120 | post_data = '' 121 | form = view.get_form(page, QueryDict(post_data)) 122 | if not form.is_valid(): 123 | raise ValueError("Invalid preview data") 124 | form.save(commit=False) 125 | return page 126 | 127 | 128 | if RELAY: 129 | class PageConnection(graphene.relay.Connection): 130 | class Meta: 131 | node = Page 132 | 133 | class Edge: 134 | pass 135 | 136 | 137 | def PagesQueryMixin(): # noqa: C901 138 | class Mixin: 139 | if RELAY: 140 | pages = graphene.ConnectionField(PageConnection) 141 | else: 142 | pages = graphene.List(Page, parent=graphene.Int()) 143 | 144 | page = graphene.Field(Page, 145 | id=graphene.Int(), 146 | url=graphene.String(), 147 | revision=graphene.Int(), 148 | ) 149 | preview = graphene.Field(Page, 150 | id=graphene.Int(required=True), 151 | ) 152 | 153 | preview_add = graphene.Field(Page, 154 | app_name=graphene.String(), 155 | model_name=graphene.String(), 156 | parent=graphene.Int(required=True), 157 | ) 158 | 159 | def resolve_pages(self, info: ResolveInfo, parent: int = None, **_kwargs): 160 | query = wagtailPage.objects 161 | 162 | # prefetch specific type pages 163 | selections = set(camelcase_to_underscore(f.name.value) 164 | for f in info.field_asts[0].selection_set.selections 165 | if not isinstance(f, InlineFragment)) 166 | for pf in registry.page_prefetch_fields.intersection(selections): 167 | query = query.select_related(pf) 168 | 169 | if parent is not None: 170 | parent_page = wagtailPage.objects.filter(id=parent).first() 171 | if parent_page is None: 172 | raise ValueError(f'Page id={parent} not found.') 173 | query = query.child_of(parent_page) 174 | 175 | return with_page_permissions( 176 | info.context, 177 | query.specific() 178 | ).live().order_by('path').all() 179 | 180 | def resolve_page(self, info: ResolveInfo, id: int = None, url: str = None, revision: int = None): 181 | query = wagtailPage.objects 182 | if id is not None: 183 | query = query.filter(id=id) 184 | elif url is not None: 185 | url_prefix = url_prefix_for_site(info) 186 | query = query.filter(url_path=url_prefix + url.rstrip('/') + '/') 187 | else: # pragma: no cover 188 | raise ValueError("One of 'id' or 'url' must be specified") 189 | page = with_page_permissions( 190 | info.context, 191 | query.specific() 192 | ).live().first() 193 | 194 | if page is None: 195 | return None 196 | 197 | if revision is not None: 198 | if revision == -1: 199 | rev = page.get_latest_revision() 200 | else: 201 | rev = page.revisions.filter(id=revision).first() 202 | if not rev: 203 | raise ValueError("Revision %d doesn't exist" % revision) 204 | 205 | page = rev.as_page_object() 206 | page.revision = rev.id 207 | return page 208 | 209 | return page 210 | 211 | def resolve_preview(self, info: ResolveInfo, id: int): # pragma: no cover 212 | from wagtail.admin.views.pages import PreviewOnEdit 213 | request = info.context 214 | view = PreviewOnEdit(args=('%d' % id, ), request=request) 215 | return _resolve_preview(request, view) 216 | 217 | def resolve_preview_add(self, info: ResolveInfo, app_name: str = 'wagtailcore', 218 | model_name: str = 'page', parent: int = None): # pragma: no cover 219 | from wagtail.admin.views.pages import PreviewOnCreate 220 | request = info.context 221 | view = PreviewOnCreate(args=(app_name, model_name, str(parent)), request=request) 222 | page = _resolve_preview(request, view) 223 | page.id = 0 # force an id, since our schema assumes page.id is an Int! 224 | return page 225 | 226 | # Show in Menu 227 | show_in_menus = graphene.List(Page) 228 | 229 | def resolve_show_in_menus(self, info: ResolveInfo): 230 | return with_page_permissions( 231 | info.context, 232 | wagtailPage.objects.filter(show_in_menus=True) 233 | ).live().order_by('path') 234 | return Mixin 235 | 236 | 237 | def InfoQueryMixin(): 238 | class Mixin: 239 | # Root 240 | root = graphene.Field(Site) 241 | 242 | def resolve_root(self, info: ResolveInfo): 243 | user = info.context.user 244 | if user.is_superuser: 245 | return info.context.site 246 | else: 247 | return None 248 | return Mixin 249 | -------------------------------------------------------------------------------- /wagtail_graphql/types/documents.py: -------------------------------------------------------------------------------- 1 | # graphql 2 | from graphql.execution.base import ResolveInfo 3 | # graphene 4 | import graphene 5 | # graphene_django 6 | from graphene_django import DjangoObjectType 7 | # graphene_django_optimizer 8 | import graphene_django_optimizer as gql_optimizer 9 | # wagtail documents 10 | from wagtail.documents.models import Document as wagtailDocument 11 | # app 12 | from ..permissions import with_collection_permissions 13 | 14 | 15 | class Document(DjangoObjectType): 16 | class Meta: 17 | model = wagtailDocument 18 | 19 | url = graphene.String() 20 | filename = graphene.String() 21 | file_extension = graphene.String() 22 | 23 | def resolve_tags(self: wagtailDocument, _info: ResolveInfo): 24 | return self.tags.all() 25 | 26 | 27 | def DocumentQueryMixin(): 28 | class Mixin: 29 | documents = graphene.List(Document) 30 | document = graphene.Field(Document, 31 | id=graphene.Int(required=True)) 32 | 33 | def resolve_documents(self, info: ResolveInfo): 34 | return with_collection_permissions( 35 | info.context, 36 | gql_optimizer.query( 37 | wagtailDocument.objects.all(), 38 | info 39 | ) 40 | ) 41 | 42 | def resolve_document(self, info: ResolveInfo, id: int): 43 | doc = with_collection_permissions( 44 | info.context, 45 | gql_optimizer.query( 46 | wagtailDocument.objects.filter(id=id), 47 | info 48 | ) 49 | ).first() 50 | return doc 51 | return Mixin 52 | -------------------------------------------------------------------------------- /wagtail_graphql/types/forms.py: -------------------------------------------------------------------------------- 1 | # graphene 2 | import graphene 3 | # graphene_django 4 | from graphene_django.converter import String, Boolean 5 | 6 | 7 | class FormField(graphene.ObjectType): 8 | name = graphene.Field(String) 9 | field_type = graphene.Field(String) 10 | help_text = graphene.Field(String) 11 | required = graphene.Field(Boolean) 12 | choices = graphene.Field(String) 13 | default_value = graphene.Field(String) 14 | label = graphene.Field(String) 15 | 16 | 17 | class FormError(graphene.ObjectType): 18 | name = graphene.Field(String) 19 | errors = graphene.List(String) 20 | -------------------------------------------------------------------------------- /wagtail_graphql/types/images.py: -------------------------------------------------------------------------------- 1 | # graphql 2 | from graphql.execution.base import ResolveInfo 3 | # django 4 | from django.urls import reverse 5 | # graphene 6 | import graphene 7 | # graphene_django 8 | from graphene_django import DjangoObjectType 9 | from graphene_django.converter import convert_django_field 10 | # graphene_django_optimizer 11 | import graphene_django_optimizer as gql_optimizer 12 | # wagtail images 13 | from wagtail.images.models import Image as wagtailImage 14 | from wagtail.images.views.serve import generate_signature 15 | # app 16 | from ..permissions import with_collection_permissions 17 | 18 | 19 | @convert_django_field.register(wagtailImage) 20 | def convert_image(field, _registry=None): 21 | return Image(description=field.help_text, required=not field.null) # pragma: no cover 22 | 23 | 24 | class Rect(graphene.ObjectType): 25 | left = graphene.Int() 26 | top = graphene.Int() 27 | right = graphene.Int() 28 | bottom = graphene.Int() 29 | 30 | x = graphene.Int() 31 | y = graphene.Int() 32 | height = graphene.Int() 33 | width = graphene.Int() 34 | 35 | 36 | class Image(DjangoObjectType): 37 | class Meta: 38 | model = wagtailImage 39 | exclude_fields = [ 40 | 'focal_point_x', 41 | 'focal_point_y', 42 | 'focal_point_width', 43 | 'focal_point_height', 44 | ] 45 | 46 | has_focal_point = graphene.Boolean() 47 | focal_point = graphene.Field(Rect) 48 | 49 | url = graphene.String(rendition=graphene.String()) 50 | url_link = graphene.String(rendition=graphene.String()) 51 | 52 | def resolve_has_focal_point(self: wagtailImage, _info: ResolveInfo): 53 | return self.has_focal_point() 54 | 55 | def resolve_focal_point(self: wagtailImage, _info: ResolveInfo): 56 | return self.get_focal_point() 57 | 58 | def resolve_tags(self: wagtailImage, _info: ResolveInfo): 59 | return self.tags.all() 60 | 61 | def resolve_url(self: wagtailImage, _info: ResolveInfo, rendition: str = None): 62 | if not rendition: 63 | if not self.has_focal_point(): 64 | rendition = "original" 65 | else: 66 | fp = self.get_focal_point() 67 | rendition = 'fill-%dx%d-c100' % (fp.width, fp.height) 68 | 69 | return generate_image_url(self, rendition) 70 | 71 | def resolve_url_link(self: wagtailImage, _info: ResolveInfo, rendition: str = None): 72 | if not rendition: 73 | if not self.has_focal_point(): 74 | rendition = "original" 75 | else: 76 | fp = self.get_focal_point() 77 | rendition = 'fill-%dx%d-c100' % (fp.width, fp.height) 78 | return self.get_rendition(rendition).url 79 | 80 | 81 | def generate_image_url(image: wagtailImage, filter_spec: str) -> str: 82 | signature = generate_signature(image.pk, filter_spec) 83 | url = reverse('wagtailimages_serve', args=(signature, image.pk, filter_spec)) 84 | return url 85 | 86 | 87 | def ImageQueryMixin(): 88 | class Mixin: 89 | images = graphene.List(Image) 90 | image = graphene.Field(Image, 91 | id=graphene.Int(required=True)) 92 | 93 | def resolve_images(self, info: ResolveInfo): 94 | return with_collection_permissions( 95 | info.context, 96 | gql_optimizer.query( 97 | wagtailImage.objects.all(), 98 | info 99 | ) 100 | ) 101 | 102 | def resolve_image(self, info: ResolveInfo, id: int): 103 | image = with_collection_permissions( 104 | info.context, 105 | gql_optimizer.query( 106 | wagtailImage.objects.filter(id=id), 107 | info 108 | ) 109 | ).first() 110 | return image 111 | return Mixin 112 | -------------------------------------------------------------------------------- /wagtail_graphql/types/menus.py: -------------------------------------------------------------------------------- 1 | # python 2 | from typing import List 3 | # graphql 4 | from graphql import ResolveInfo 5 | # graphene_django 6 | import graphene 7 | from graphene_django import DjangoObjectType 8 | # wagtailmenus 9 | from wagtailmenus.models import FlatMenu, FlatMenuItem, MainMenu, MainMenuItem 10 | 11 | 12 | class MenuItem(DjangoObjectType): 13 | class Meta: 14 | model = MainMenuItem 15 | 16 | 17 | class Menu(DjangoObjectType): 18 | class Meta: 19 | model = MainMenu 20 | only_fields = ['max_levels', 'menu_items'] 21 | 22 | 23 | class SecondaryMenuItem(DjangoObjectType): 24 | class Meta: 25 | model = FlatMenuItem 26 | 27 | 28 | class SecondaryMenu(DjangoObjectType): 29 | class Meta: 30 | model = FlatMenu 31 | only_fields = ['title', 'handle', 'heading', 'max_levels', 'menu_items'] 32 | 33 | 34 | def MenusQueryMixin(): 35 | class Mixin: 36 | main_menu = graphene.List(Menu) 37 | secondary_menu = graphene.Field(SecondaryMenu, 38 | handle=graphene.String(required=True)) 39 | secondary_menus = graphene.List(SecondaryMenu) 40 | 41 | def resolve_main_menu(self, _info: ResolveInfo) -> List[MainMenu]: 42 | return MainMenu.objects.all() 43 | 44 | def resolve_secondary_menus(self, _info: ResolveInfo) -> List[FlatMenu]: 45 | return FlatMenu.objects.all() 46 | 47 | def resolve_secondary_menu(self, _info, handle: ResolveInfo) -> FlatMenu: 48 | return FlatMenu.objects.filter(handle=handle).first() 49 | return Mixin 50 | -------------------------------------------------------------------------------- /wagtail_graphql/types/settings.py: -------------------------------------------------------------------------------- 1 | # graphql 2 | from graphql.execution.base import ResolveInfo 3 | # graphene 4 | import graphene 5 | # graphene_django 6 | from graphene_django.converter import String 7 | # app 8 | from ..registry import registry 9 | from ..settings import settings_registry 10 | 11 | 12 | class Settings(graphene.Interface): 13 | __typename = graphene.Field(String) 14 | 15 | 16 | def SettingsQueryMixin(): 17 | class Mixin: 18 | if settings_registry: 19 | settings = graphene.Field(Settings, 20 | name=graphene.String(required=True)) 21 | 22 | def resolve_settings(self, _info: ResolveInfo, name): 23 | try: 24 | result = registry.settings[name][1].objects.first() 25 | except KeyError: 26 | raise ValueError(f"Settings '{name}' not found.") 27 | return result 28 | else: # pragma: no cover 29 | pass 30 | return Mixin 31 | -------------------------------------------------------------------------------- /wagtail_graphql/types/snippets.py: -------------------------------------------------------------------------------- 1 | # django 2 | from django.db import models 3 | # graphql 4 | from graphql.execution.base import ResolveInfo 5 | # graphene 6 | import graphene 7 | # app 8 | from ..registry import registry 9 | 10 | 11 | def SnippetsQueryMixin(): 12 | class Mixin: 13 | if registry.snippets: 14 | class Snippet(graphene.types.union.Union): 15 | class Meta: 16 | types = registry.snippets.types 17 | 18 | snippets = graphene.List(Snippet, 19 | typename=graphene.String(required=True)) 20 | 21 | def resolve_snippets(self, _info: ResolveInfo, typename: str) -> models.Model: 22 | node = registry.snippets_by_name[typename] 23 | cls = node._meta.model 24 | return cls.objects.all() 25 | else: # pragma: no cover 26 | pass 27 | return Mixin 28 | -------------------------------------------------------------------------------- /wagtail_graphql/types/streamfield.py: -------------------------------------------------------------------------------- 1 | # python 2 | from typing import Tuple, Callable 3 | import datetime 4 | # graphql 5 | from graphql import GraphQLScalarType 6 | from graphql.execution.base import ResolveInfo 7 | # graphene 8 | import graphene 9 | from graphene.utils.str_converters import to_snake_case 10 | from graphene.types.generic import GenericScalar 11 | from graphene.types import Scalar 12 | # graphene_django 13 | from graphene_django.converter import convert_django_field, List 14 | # dateutil 15 | from dateutil.parser import parse as dtparse 16 | # wagtail 17 | import wagtail.core.blocks 18 | import wagtail.images.blocks 19 | import wagtail.snippets.blocks 20 | from wagtail.core.blocks import Block, ListBlock, StructBlock 21 | from wagtail.core.fields import StreamField 22 | # app 23 | from ..registry import registry 24 | from .. import settings 25 | # app types 26 | from .core import Page, wagtailPage 27 | from .images import Image, wagtailImage 28 | 29 | # types 30 | StreamFieldHandlerType = Tuple[graphene.List, Callable[[StreamField, ResolveInfo], list]] 31 | 32 | 33 | @convert_django_field.register(StreamField) 34 | def convert_stream_field(field, _registry=None): 35 | return Scalar(description=field.help_text, required=not field.null) 36 | 37 | 38 | def _scalar_block(graphene_type): 39 | tp = registry.scalar_blocks.get(graphene_type) 40 | if not tp: 41 | node = '%sBlock' % graphene_type 42 | tp = type(node, (graphene.ObjectType,), { 43 | 'value': graphene.Field(graphene_type), 44 | 'field': graphene.Field(graphene.String), 45 | }) 46 | registry.scalar_blocks[graphene_type] = tp 47 | return tp 48 | 49 | 50 | def _resolve_scalar(key, type_): 51 | type_str = str(type_) 52 | if type_str == 'DateBlock': 53 | def resolve(self, _info: ResolveInfo): 54 | return type_(value=dtparse(self), field=key) 55 | elif type_str == 'DateTimeBlock': 56 | def resolve(self, _info: ResolveInfo): 57 | return type_(value=dtparse(self), field=key) 58 | elif type_str == 'TimeBlock': 59 | def resolve(self, _info: ResolveInfo): 60 | return type_(value=datetime.time.fromisoformat(self), field=key) 61 | else: 62 | def resolve(self, _info: ResolveInfo): 63 | return type_(value=self, field=key) 64 | return resolve 65 | 66 | 67 | def _page_block(): 68 | tp = registry.scalar_blocks.get(Page) 69 | if not tp: 70 | node = 'PageBlock' 71 | tp = type(node, (graphene.ObjectType,), { 72 | 'value': graphene.Field(Page), 73 | 'field': graphene.Field(graphene.String), 74 | }) 75 | registry.scalar_blocks[Page] = tp 76 | return tp 77 | 78 | 79 | def _resolve_page_block(key, type_): 80 | def resolve(self, info: ResolveInfo): 81 | return type_(value=_resolve_page(self, info), field=key) 82 | return resolve 83 | 84 | 85 | def _image_block(): 86 | tp = registry.scalar_blocks.get(Image) 87 | if not tp: 88 | node = 'ImageBlock' 89 | tp = type(node, (graphene.ObjectType,), { 90 | 'value': graphene.Field(Image), 91 | 'field': graphene.Field(graphene.String), 92 | }) 93 | registry.scalar_blocks[Image] = tp 94 | return tp 95 | 96 | 97 | def _resolve_image_block(key, type_): 98 | def resolve(self, info: ResolveInfo): 99 | return type_(value=_resolve_image(self, info), field=key) 100 | return resolve 101 | 102 | 103 | def _snippet_block(typ): 104 | tp = registry.scalar_blocks.get(typ) 105 | if not tp: 106 | node = '%sBlock' % typ 107 | tp = type(node, (graphene.ObjectType,), { 108 | 'value': graphene.Field(typ), 109 | 'field': graphene.Field(graphene.String), 110 | }) 111 | registry.scalar_blocks[typ] = tp 112 | return tp 113 | 114 | 115 | def _resolve_snippet_block(key, type_, snippet_type): 116 | def resolve(self, info: ResolveInfo): 117 | info.return_type = snippet_type 118 | return type_(value=_resolve_snippet(self, info), field=key) 119 | return resolve 120 | 121 | 122 | def _list_block(typ): 123 | tp = registry.scalar_blocks.get((List, typ)) 124 | if not tp: 125 | node = '%sListBlock' % typ 126 | tp = type(node, (graphene.ObjectType,), { 127 | 'value': graphene.List(typ), 128 | 'field': graphene.Field(graphene.String), 129 | }) 130 | registry.scalar_blocks[(List, typ)] = tp 131 | return tp 132 | 133 | 134 | def _resolve_list_block_scalar(key, type_, of_type): 135 | type_str = str(of_type) 136 | if type_str == 'Date' or type_str == 'DateTime': 137 | def resolve(self, _info: ResolveInfo): 138 | return type_(value=list(dtparse(s) for s in self), field=key) 139 | elif type_str == 'Time': 140 | def resolve(self, _info: ResolveInfo): 141 | return type_(value=list(datetime.time.fromisoformat(s) for s in self), field=key) 142 | else: 143 | def resolve(self, _info: ResolveInfo): 144 | return type_(value=list(s for s in self), field=key) 145 | return resolve 146 | 147 | 148 | def _resolve_list_block(key, type_, of_type): 149 | if issubclass(of_type, Scalar): 150 | resolve = _resolve_list_block_scalar(key, type_, of_type) 151 | elif of_type == Image: 152 | def resolve(self, info: ResolveInfo): 153 | return type_(value=list(_resolve_image(s, info) for s in self), field=key) 154 | elif of_type == Page: 155 | def resolve(self, info: ResolveInfo): 156 | return type_(value=list(_resolve_page(s, info) for s in self), field=key) 157 | elif of_type in registry.snippets.values(): 158 | def resolve(self, info: ResolveInfo): 159 | info.return_type = of_type 160 | return type_(value=list(_resolve_snippet(s, info) for s in self), field=key) 161 | else: 162 | def resolve(self, info: ResolveInfo): 163 | info.return_type = of_type 164 | return type_(value=list(of_type(**s) for s in self), field=key) 165 | return resolve 166 | 167 | 168 | def _create_root_blocks(block_type_handlers: dict): 169 | for k, t in block_type_handlers.items(): 170 | if not isinstance(t, tuple) and issubclass(t, Scalar): 171 | typ = _scalar_block(t) 172 | block_type_handlers[k] = typ, _resolve_scalar(k, typ) 173 | elif isinstance(t, tuple) and isinstance(t[0], List): 174 | typ = _list_block(t[0].of_type) 175 | block_type_handlers[k] = typ, _resolve_list_block(k, typ, t[0].of_type) 176 | elif isinstance(t, tuple) and issubclass(t[0], Page): 177 | typ = _page_block() 178 | block_type_handlers[k] = typ, _resolve_page_block(k, typ) 179 | elif isinstance(t, tuple) and issubclass(t[0], Image): 180 | typ = _image_block() 181 | block_type_handlers[k] = typ, _resolve_image_block(k, typ) 182 | elif isinstance(t, tuple) and t[0] in registry.snippets.values(): 183 | typ = _snippet_block(t[0]) 184 | block_type_handlers[k] = typ, _resolve_snippet_block(k, typ, t[0]) 185 | 186 | 187 | def convert_block(block, block_type_handlers: dict, info: ResolveInfo, is_lazy=True): 188 | if is_lazy: 189 | block_type = block.get('type') 190 | value = block.get('value') 191 | else: 192 | block_type, value = block[:2] 193 | if block_type in block_type_handlers: 194 | handler = block_type_handlers[block_type] 195 | if isinstance(handler, tuple): 196 | tp, resolver = handler 197 | return resolver(value, info) 198 | else: 199 | if isinstance(value, dict): 200 | return handler(**value) 201 | else: 202 | raise NotImplementedError() # pragma: no cover 203 | else: 204 | raise NotImplementedError() # pragma: no cover 205 | 206 | 207 | def _resolve_type(self, _info: ResolveInfo): 208 | return self.__class__ 209 | 210 | 211 | def stream_field_handler(stream_field_name: str, field_name: str, block_type_handlers: dict) -> StreamFieldHandlerType: 212 | # add Generic Scalars (default) 213 | if settings.LOAD_GENERIC_SCALARS: 214 | _scalar_block(GenericScalar) 215 | 216 | # Unions must reference NamedTypes, so for scalar types we need to create a new type to 217 | # encapsulate scalars, page links, images, snippets 218 | _create_root_blocks(block_type_handlers) 219 | 220 | types_ = list(block_type_handlers.values()) 221 | for i, t in enumerate(types_): 222 | if isinstance(t, tuple): 223 | types_[i] = t[0] 224 | 225 | class Meta: 226 | types = tuple(set(types_)) 227 | 228 | stream_field_type = type( 229 | stream_field_name + "Type", 230 | (graphene.Union, ), 231 | { 232 | 'Meta': Meta, 233 | 'resolve_type': _resolve_type 234 | } 235 | ) 236 | 237 | def resolve_field(self, info: ResolveInfo): 238 | field = getattr(self, field_name) 239 | return [convert_block(block, block_type_handlers, info, field.is_lazy) for block in field.stream_data] 240 | 241 | return graphene.List(stream_field_type), resolve_field 242 | 243 | 244 | def _is_compound_block(block): 245 | return isinstance(block, StructBlock) 246 | 247 | 248 | def _is_list_block(block): 249 | return isinstance(block, ListBlock) 250 | 251 | 252 | def _is_custom_type(block): 253 | return hasattr(block, "__graphql_type__") 254 | 255 | 256 | def _add_handler_resolves(dict_params): 257 | to_add = {} 258 | for k, v in dict_params.items(): 259 | if k == 'field': # pragma: no cover 260 | raise ValueError("StructBlocks cannot have fields named 'field'") 261 | if isinstance(v, tuple): 262 | val = v[0] 263 | to_add['resolve_' + k] = v[1] 264 | elif issubclass(v, (graphene.types.DateTime, graphene.types.Date)): 265 | val = v 266 | to_add['resolve_' + k] = _resolve_datetime 267 | elif issubclass(v, graphene.types.Time): 268 | val = v 269 | to_add['resolve_' + k] = _resolve_time 270 | elif not issubclass(v, Scalar): 271 | val = v 272 | to_add['resolve_' + k] = _resolve 273 | else: 274 | val = v 275 | dict_params[k] = graphene.Field(val) 276 | dict_params.update(to_add) 277 | 278 | 279 | def block_handler(block: Block, app, prefix=''): 280 | cls = block.__class__ 281 | handler = registry.blocks.get(cls) 282 | 283 | if handler is None: 284 | if _is_custom_type(block): 285 | target_block_type = block.__graphql_type__() 286 | this_handler = block_handler(target_block_type, app, prefix) 287 | if isinstance(this_handler, tuple): 288 | raise NotImplementedError() 289 | if hasattr(block, '__graphql_resolve__'): 290 | resolver = _resolve_custom(block, this_handler) 291 | elif issubclass(target_block_type, Scalar): 292 | resolver = _resolve_generic_scalar 293 | else: 294 | raise TypeError("Non Scalar custom types need an explicit __graphql_resolve__ method.") 295 | handler = (lambda x: this_handler, resolver) 296 | elif _is_compound_block(block): 297 | node = prefix + cls.__name__ 298 | dict_params = dict( 299 | (n, block_handler(block_type, app, prefix)) 300 | for n, block_type in block.child_blocks.items() 301 | ) 302 | _add_handler_resolves(dict_params) 303 | dict_params.update({ # add the field name 304 | 'field': graphene.Field(graphene.String), 305 | 'resolve_field': lambda *x: block.name, 306 | }) 307 | tp = type(node, (graphene.ObjectType,), dict_params) 308 | handler = tp 309 | registry.blocks[cls] = handler 310 | elif _is_list_block(block): 311 | this_handler = block_handler(block.child_block, app, prefix) 312 | if isinstance(this_handler, tuple): 313 | handler = List(this_handler[0]), _resolve_list(*this_handler) 314 | else: 315 | handler = List(this_handler), _resolve_simple_list 316 | else: 317 | handler = GenericScalar 318 | 319 | if cls == wagtail.snippets.blocks.SnippetChooserBlock: 320 | handler = (handler[0](block), handler[1]) # type: ignore 321 | 322 | return handler 323 | 324 | 325 | def _snippet_handler(block): 326 | tp = registry.snippets[block.target_model] 327 | return tp 328 | 329 | 330 | def _resolve_snippet(self, info: ResolveInfo): 331 | if self is None: 332 | return None 333 | field = to_snake_case(info.field_name) 334 | id_ = self if isinstance(self, int) else getattr(self, field) 335 | if hasattr(info.return_type, 'graphene_type'): 336 | cls = info.return_type.graphene_type._meta.model 337 | else: 338 | cls = info.return_type._meta.model 339 | obj = cls.objects.filter(id=id_).first() 340 | return obj 341 | 342 | 343 | def _resolve_image(self, info: ResolveInfo): 344 | if self is None: 345 | return None 346 | field = to_snake_case(info.field_name) 347 | id_ = self if isinstance(self, int) else getattr(self, field) 348 | return wagtailImage.objects.filter(id=id_).first() 349 | 350 | 351 | def _resolve_page(self, info: ResolveInfo): 352 | if self is None: 353 | return None 354 | field = to_snake_case(info.field_name) 355 | id_ = self if isinstance(self, int) else getattr(self, field) 356 | return wagtailPage.objects.filter(id=id_).specific().first() 357 | 358 | 359 | def _resolve(self, info: ResolveInfo): 360 | if self is None: 361 | return None 362 | field = to_snake_case(info.field_name) 363 | data = getattr(self, field) 364 | cls = info.return_type 365 | return cls.graphene_type(**data) 366 | 367 | 368 | def _resolve_datetime(self, info: ResolveInfo): 369 | if self is None: 370 | return None 371 | field = to_snake_case(info.field_name) 372 | data = getattr(self, field) 373 | return dtparse(data) if data else None 374 | 375 | 376 | def _resolve_time(self, info: ResolveInfo): 377 | if self is None: 378 | return None 379 | field = to_snake_case(info.field_name) 380 | data = getattr(self, field) 381 | return datetime.time.fromisoformat(data) if data else None 382 | 383 | 384 | def _resolve_custom(block, hdl): 385 | def _inner(self, info: ResolveInfo): 386 | if self is None: 387 | return None 388 | cls = info.return_type 389 | if isinstance(self, dict): 390 | data = self 391 | else: 392 | data = getattr(self, info.field_name) 393 | value = block.__graphql_resolve__(data, info) 394 | 395 | if hasattr(cls, "serialize"): 396 | return cls.serialize(value) 397 | return hdl(**value) 398 | return _inner 399 | 400 | 401 | def _resolve_generic_scalar(self, info: ResolveInfo): 402 | if self is None: 403 | return None 404 | data = getattr(self, info.field_name) 405 | cls = info.return_type 406 | return cls.serialize(data) 407 | 408 | 409 | def _resolve_simple_list(self, info: ResolveInfo): 410 | if self is None: 411 | return None 412 | field = to_snake_case(info.field_name) 413 | data = getattr(self, field) 414 | cls = info.return_type.of_type 415 | if isinstance(cls, (Scalar, GraphQLScalarType)): 416 | return list(d for d in data) 417 | return list(cls(**d) for d in data) 418 | 419 | 420 | def _resolve_list(tp, inner_resolver): 421 | def resolve(self, info: ResolveInfo): 422 | if self is None: 423 | return None 424 | field = to_snake_case(info.field_name) 425 | ids = getattr(self, field) 426 | info.return_type = tp 427 | return list(inner_resolver(i, info) for i in ids) 428 | return resolve 429 | 430 | 431 | registry.blocks.update({ 432 | # choosers 433 | wagtail.images.blocks.ImageChooserBlock: (Image, _resolve_image), 434 | wagtail.core.blocks.PageChooserBlock: (Page, _resolve_page), 435 | wagtail.snippets.blocks.SnippetChooserBlock: (_snippet_handler, _resolve_snippet), 436 | # standard fields 437 | wagtail.core.blocks.CharBlock: graphene.types.String, 438 | wagtail.core.blocks.URLBlock: graphene.types.String, 439 | wagtail.core.blocks.DateBlock: graphene.types.Date, 440 | wagtail.core.blocks.DateTimeBlock: graphene.types.DateTime, 441 | wagtail.core.blocks.BooleanBlock: graphene.types.Boolean, 442 | wagtail.core.blocks.IntegerBlock: graphene.types.Int, 443 | wagtail.core.blocks.FloatBlock: graphene.types.Float, 444 | wagtail.core.blocks.DecimalBlock: graphene.types.String, 445 | wagtail.core.blocks.TextBlock: graphene.types.String, 446 | wagtail.core.blocks.TimeBlock: graphene.types.Time, 447 | wagtail.core.blocks.RichTextBlock: graphene.types.String, 448 | wagtail.core.blocks.RawHTMLBlock: graphene.types.String, 449 | wagtail.core.blocks.BlockQuoteBlock: graphene.types.String, 450 | wagtail.core.blocks.ChoiceBlock: graphene.types.String, 451 | wagtail.core.blocks.RegexBlock: graphene.types.String, 452 | wagtail.core.blocks.EmailBlock: graphene.types.String, 453 | wagtail.core.blocks.StaticBlock: graphene.types.String, 454 | }) 455 | --------------------------------------------------------------------------------