diff --git a/docs/getting-started/settings.rst b/docs/getting-started/settings.rst index 8d3d0aad..23f37179 100644 --- a/docs/getting-started/settings.rst +++ b/docs/getting-started/settings.rst @@ -152,4 +152,4 @@ Wagtail Page interface Used to construct the schema for Wagtail Page-derived models. It can be overridden to provide a custom interface for all page models. -Default: ``grapple.types.pages.PageInterface`` +Default: ``grapple.types.interfaces.PageInterface`` diff --git a/grapple/models.py b/grapple/models.py index 5c8d9920..2b094338 100644 --- a/grapple/models.py +++ b/grapple/models.py @@ -151,7 +151,7 @@ def Mixin(): def GraphQLPage(field_name: str, **kwargs): def Mixin(): - from .types.pages import get_page_interface + from .types.interfaces import get_page_interface return GraphQLField(field_name, get_page_interface(), **kwargs) diff --git a/grapple/settings.py b/grapple/settings.py index c543eca5..3c5d10ae 100644 --- a/grapple/settings.py +++ b/grapple/settings.py @@ -28,7 +28,7 @@ "PAGE_SIZE": 10, "MAX_PAGE_SIZE": 100, "RICHTEXT_FORMAT": "html", - "PAGE_INTERFACE": "grapple.types.pages.PageInterface", + "PAGE_INTERFACE": "grapple.types.interfaces.PageInterface", } # List of settings that have been deprecated diff --git a/grapple/types/interfaces.py b/grapple/types/interfaces.py new file mode 100644 index 00000000..e67739bb --- /dev/null +++ b/grapple/types/interfaces.py @@ -0,0 +1,160 @@ +import graphene + +from django.contrib.contenttypes.models import ContentType +from django.utils.module_loading import import_string +from graphql import GraphQLError +from wagtail.models import Page as WagtailPage + +from ..registry import registry +from ..settings import grapple_settings +from ..utils import resolve_queryset +from .structures import QuerySetList + + +def get_page_interface(): + return import_string(grapple_settings.PAGE_INTERFACE) + + +class PageInterface(graphene.Interface): + id = graphene.ID() + title = graphene.String(required=True) + slug = graphene.String(required=True) + content_type = graphene.String(required=True) + page_type = graphene.String() + live = graphene.Boolean(required=True) + + url = graphene.String() + url_path = graphene.String(required=True) + + depth = graphene.Int() + seo_title = graphene.String(required=True) + search_description = graphene.String() + show_in_menus = graphene.Boolean(required=True) + + locked = graphene.Boolean() + + first_published_at = graphene.DateTime() + last_published_at = graphene.DateTime() + + parent = graphene.Field(get_page_interface) + children = QuerySetList( + graphene.NonNull(get_page_interface), enable_search=True, required=True + ) + siblings = QuerySetList( + graphene.NonNull(get_page_interface), enable_search=True, required=True + ) + next_siblings = QuerySetList( + graphene.NonNull(get_page_interface), enable_search=True, required=True + ) + previous_siblings = QuerySetList( + graphene.NonNull(get_page_interface), enable_search=True, required=True + ) + descendants = QuerySetList( + graphene.NonNull(get_page_interface), enable_search=True, required=True + ) + ancestors = QuerySetList( + graphene.NonNull(get_page_interface), enable_search=True, required=True + ) + + search_score = graphene.Float() + + @classmethod + def resolve_type(cls, instance, info, **kwargs): + """ + If model has a custom Graphene Node type in registry then use it, + otherwise use base page type. + """ + from .pages import Page + + return registry.pages.get(type(instance), Page) + + def resolve_content_type(self, info, **kwargs): + self.content_type = ContentType.objects.get_for_model(self) + return ( + f"{self.content_type.app_label}.{self.content_type.model_class().__name__}" + ) + + def resolve_page_type(self, info, **kwargs): + return get_page_interface().resolve_type(self.specific, info, **kwargs) + + def resolve_parent(self, info, **kwargs): + """ + Resolves the parent node of current page node. + Docs: https://docs.wagtail.io/en/stable/reference/pages/model_reference.html#wagtail.models.Page.get_parent + """ + try: + return self.get_parent().specific + except GraphQLError: + return WagtailPage.objects.none() + + def resolve_children(self, info, **kwargs): + """ + Resolves a list of live children of this page. + Docs: https://docs.wagtail.io/en/stable/reference/pages/queryset_reference.html#examples + """ + return resolve_queryset( + self.get_children().live().public().specific(), info, **kwargs + ) + + def resolve_siblings(self, info, **kwargs): + """ + Resolves a list of sibling nodes to this page. + Docs: https://docs.wagtail.io/en/stable/reference/pages/queryset_reference.html?highlight=get_siblings#wagtail.query.PageQuerySet.sibling_of + """ + return resolve_queryset( + self.get_siblings().exclude(pk=self.pk).live().public().specific(), + info, + **kwargs, + ) + + def resolve_next_siblings(self, info, **kwargs): + """ + Resolves a list of direct next siblings of this page. Similar to `resolve_siblings` with sorting. + Source: https://github.com/wagtail/wagtail/blob/master/wagtail/core/models.py#L1384 + """ + return resolve_queryset( + self.get_next_siblings().exclude(pk=self.pk).live().public().specific(), + info, + **kwargs, + ) + + def resolve_previous_siblings(self, info, **kwargs): + """ + Resolves a list of direct prev siblings of this page. Similar to `resolve_siblings` with sorting. + Source: https://github.com/wagtail/wagtail/blob/master/wagtail/core/models.py#L1387 + """ + return resolve_queryset( + self.get_prev_siblings().exclude(pk=self.pk).live().public().specific(), + info, + **kwargs, + ) + + def resolve_descendants(self, info, **kwargs): + """ + Resolves a list of nodes pointing to the current page’s descendants. + Docs: https://docs.wagtail.io/en/stable/reference/pages/model_reference.html#wagtail.models.Page.get_descendants + """ + return resolve_queryset( + self.get_descendants().live().public().specific(), info, **kwargs + ) + + def resolve_ancestors(self, info, **kwargs): + """ + Resolves a list of nodes pointing to the current page’s ancestors. + Docs: https://docs.wagtail.io/en/stable/reference/pages/model_reference.html#wagtail.models.Page.get_ancestors + """ + return resolve_queryset( + self.get_ancestors().live().public().specific(), info, **kwargs + ) + + def resolve_seo_title(self, info, **kwargs): + """ + Get page's SEO title. Fallback to a normal page's title if absent. + """ + return self.seo_title or self.title + + def resolve_search_score(self, info, **kwargs): + """ + Get page's search score, will be None if not in a search context. + """ + return getattr(self, "search_score", None) diff --git a/grapple/types/pages.py b/grapple/types/pages.py index 58a460db..98e35456 100644 --- a/grapple/types/pages.py +++ b/grapple/types/pages.py @@ -2,7 +2,6 @@ from django.contrib.contenttypes.models import ContentType from django.db.models import Q -from django.utils.module_loading import import_string from django.utils.translation import gettext_lazy as _ from graphene_django.types import DjangoObjectType from graphql import GraphQLError @@ -10,158 +9,11 @@ from wagtail.models import Site from ..registry import registry -from ..settings import grapple_settings from ..utils import resolve_queryset, resolve_site_by_hostname +from .interfaces import get_page_interface from .structures import QuerySetList -def get_page_interface(): - return import_string(grapple_settings.PAGE_INTERFACE) - - -class PageInterface(graphene.Interface): - id = graphene.ID() - title = graphene.String(required=True) - slug = graphene.String(required=True) - content_type = graphene.String(required=True) - page_type = graphene.String() - live = graphene.Boolean(required=True) - - url = graphene.String() - url_path = graphene.String(required=True) - - depth = graphene.Int() - seo_title = graphene.String(required=True) - search_description = graphene.String() - show_in_menus = graphene.Boolean(required=True) - - locked = graphene.Boolean() - - first_published_at = graphene.DateTime() - last_published_at = graphene.DateTime() - - parent = graphene.Field(get_page_interface) - children = QuerySetList( - graphene.NonNull(get_page_interface), enable_search=True, required=True - ) - siblings = QuerySetList( - graphene.NonNull(get_page_interface), enable_search=True, required=True - ) - next_siblings = QuerySetList( - graphene.NonNull(get_page_interface), enable_search=True, required=True - ) - previous_siblings = QuerySetList( - graphene.NonNull(get_page_interface), enable_search=True, required=True - ) - descendants = QuerySetList( - graphene.NonNull(get_page_interface), enable_search=True, required=True - ) - ancestors = QuerySetList( - graphene.NonNull(get_page_interface), enable_search=True, required=True - ) - - search_score = graphene.Float() - - @classmethod - def resolve_type(cls, instance, info, **kwargs): - """ - If model has a custom Graphene Node type in registry then use it, - otherwise use base page type. - """ - return registry.pages.get(type(instance), Page) - - def resolve_content_type(self, info, **kwargs): - self.content_type = ContentType.objects.get_for_model(self) - return ( - f"{self.content_type.app_label}.{self.content_type.model_class().__name__}" - ) - - def resolve_page_type(self, info, **kwargs): - return get_page_interface().resolve_type(self.specific, info, **kwargs) - - def resolve_parent(self, info, **kwargs): - """ - Resolves the parent node of current page node. - Docs: https://docs.wagtail.io/en/stable/reference/pages/model_reference.html#wagtail.models.Page.get_parent - """ - try: - return self.get_parent().specific - except GraphQLError: - return WagtailPage.objects.none() - - def resolve_children(self, info, **kwargs): - """ - Resolves a list of live children of this page. - Docs: https://docs.wagtail.io/en/stable/reference/pages/queryset_reference.html#examples - """ - return resolve_queryset( - self.get_children().live().public().specific(), info, **kwargs - ) - - def resolve_siblings(self, info, **kwargs): - """ - Resolves a list of sibling nodes to this page. - Docs: https://docs.wagtail.io/en/stable/reference/pages/queryset_reference.html?highlight=get_siblings#wagtail.query.PageQuerySet.sibling_of - """ - return resolve_queryset( - self.get_siblings().exclude(pk=self.pk).live().public().specific(), - info, - **kwargs, - ) - - def resolve_next_siblings(self, info, **kwargs): - """ - Resolves a list of direct next siblings of this page. Similar to `resolve_siblings` with sorting. - Source: https://github.com/wagtail/wagtail/blob/master/wagtail/core/models.py#L1384 - """ - return resolve_queryset( - self.get_next_siblings().exclude(pk=self.pk).live().public().specific(), - info, - **kwargs, - ) - - def resolve_previous_siblings(self, info, **kwargs): - """ - Resolves a list of direct prev siblings of this page. Similar to `resolve_siblings` with sorting. - Source: https://github.com/wagtail/wagtail/blob/master/wagtail/core/models.py#L1387 - """ - return resolve_queryset( - self.get_prev_siblings().exclude(pk=self.pk).live().public().specific(), - info, - **kwargs, - ) - - def resolve_descendants(self, info, **kwargs): - """ - Resolves a list of nodes pointing to the current page’s descendants. - Docs: https://docs.wagtail.io/en/stable/reference/pages/model_reference.html#wagtail.models.Page.get_descendants - """ - return resolve_queryset( - self.get_descendants().live().public().specific(), info, **kwargs - ) - - def resolve_ancestors(self, info, **kwargs): - """ - Resolves a list of nodes pointing to the current page’s ancestors. - Docs: https://docs.wagtail.io/en/stable/reference/pages/model_reference.html#wagtail.models.Page.get_ancestors - """ - return resolve_queryset( - self.get_ancestors().live().public().specific(), info, **kwargs - ) - - def resolve_seo_title(self, info, **kwargs): - """ - Get page's SEO title. Fallback to a normal page's title if absent. - """ - return self.seo_title or self.title - - def resolve_search_score(self, info, **kwargs): - """ - Get page's search score, will be None if not in a search context. - """ - return getattr(self, "search_score", None) - - class Page(DjangoObjectType): """ Base Page type used if one isn't generated for the current model. diff --git a/grapple/types/redirects.py b/grapple/types/redirects.py index 540f3f0b..23085f23 100644 --- a/grapple/types/redirects.py +++ b/grapple/types/redirects.py @@ -9,7 +9,7 @@ from grapple.types.sites import SiteObjectType -from .pages import get_page_interface +from .interfaces import get_page_interface class RedirectObjectType(graphene.ObjectType): diff --git a/grapple/types/sites.py b/grapple/types/sites.py index 87c9c08c..51358fd8 100644 --- a/grapple/types/sites.py +++ b/grapple/types/sites.py @@ -10,7 +10,8 @@ from wagtail.models import Site from ..utils import resolve_queryset, resolve_site_by_hostname, resolve_site_by_id -from .pages import get_page_interface, get_specific_page +from .interfaces import get_page_interface +from .pages import get_specific_page from .structures import QuerySetList diff --git a/grapple/types/streamfield.py b/grapple/types/streamfield.py index f979ffbd..3b78fa3c 100644 --- a/grapple/types/streamfield.py +++ b/grapple/types/streamfield.py @@ -437,7 +437,7 @@ def resolve_items(self, info, **kwargs): def register_streamfield_blocks(): from .documents import get_document_type from .images import get_image_type - from .pages import get_page_interface + from .interfaces import get_page_interface from .snippets import SnippetTypes class PageChooserBlock(graphene.ObjectType): diff --git a/tests/test_interfaces.py b/tests/test_interfaces.py index 1ab7d826..0a8b6158 100644 --- a/tests/test_interfaces.py +++ b/tests/test_interfaces.py @@ -7,7 +7,7 @@ from testapp.factories import BlogPageFactory, CustomInterfaceBlockFactory from testapp.interfaces import CustomInterface -from grapple.types.pages import PageInterface, get_page_interface +from grapple.types.interfaces import PageInterface, get_page_interface @skipIf( diff --git a/tests/testapp/mutations.py b/tests/testapp/mutations.py index 48cfc619..3e8f785a 100644 --- a/tests/testapp/mutations.py +++ b/tests/testapp/mutations.py @@ -3,7 +3,7 @@ from wagtail.models import Page from grapple.registry import registry -from grapple.types.pages import PageInterface +from grapple.types.interfaces import PageInterface from grapple.types.rich_text import RichText from testapp.models import Advert, AuthorPage