Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix circular import for PageInterface when using custom page interface #404

Merged
merged 7 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### Fixed

- `value` not being queryable on `EmbedBlock` ([#399](https://github.com/torchbox/wagtail-grapple/pull/399))@JakubMastalerz
- Circular import when defining custom `PageInterface` ([#404](https://github.com/torchbox/wagtail-grapple/pull/404)) @mgax

## [0.26.0] - 2024-06-26

Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``
2 changes: 1 addition & 1 deletion grapple/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion grapple/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
160 changes: 160 additions & 0 deletions grapple/types/interfaces.py
Original file line number Diff line number Diff line change
@@ -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)
150 changes: 1 addition & 149 deletions grapple/types/pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,166 +2,18 @@

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
from wagtail.models import Page as WagtailPage
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.
Expand Down
2 changes: 1 addition & 1 deletion grapple/types/redirects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
3 changes: 2 additions & 1 deletion grapple/types/sites.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
2 changes: 1 addition & 1 deletion grapple/types/streamfield.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
zerolab marked this conversation as resolved.
Show resolved Hide resolved


@skipIf(
Expand Down
Loading
Loading