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 all 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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ jobs:
path: tests/.coverage*
if-no-files-found: ignore
retention-days: 1
include-hidden-files: true
zerolab marked this conversation as resolved.
Show resolved Hide resolved

tests-postgres:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -105,6 +106,7 @@ jobs:
path: tests/.coverage*
if-no-files-found: ignore
retention-days: 1
include-hidden-files: true

coverage:
runs-on: ubuntu-latest
Expand Down
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
211 changes: 211 additions & 0 deletions grapple/types/interfaces.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import inspect

import graphene

from django.contrib.contenttypes.models import ContentType
from django.utils.module_loading import import_string
from graphql import GraphQLError
from wagtail import blocks
from wagtail.models import Page as WagtailPage
from wagtail.rich_text import RichText

from ..registry import registry
from ..settings import grapple_settings
from ..utils import resolve_queryset, serialize_struct_obj
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)


class StreamFieldInterface(graphene.Interface):
id = graphene.String()
block_type = graphene.String(required=True)
field = graphene.String(required=True)
raw_value = graphene.String(required=True)

@classmethod
def resolve_type(cls, instance, info):
"""
If block has a custom Graphene Node type in registry then use it,
otherwise use generic block type.
"""
if hasattr(instance, "block"):
mdl = type(instance.block)
if mdl in registry.streamfield_blocks:
return registry.streamfield_blocks[mdl]

for block_class in inspect.getmro(mdl):
if block_class in registry.streamfield_blocks:
return registry.streamfield_blocks[block_class]

return registry.streamfield_blocks["generic-block"]

def resolve_id(self, info, **kwargs):
return self.id

def resolve_block_type(self, info, **kwargs):
return type(self.block).__name__

def resolve_field(self, info, **kwargs):
return self.block.name

def resolve_raw_value(self, info, **kwargs):
if isinstance(self, blocks.StructValue):
# This is the value for a nested StructBlock defined via GraphQLStreamfield
return serialize_struct_obj(self)
elif isinstance(self.value, dict):
return serialize_struct_obj(self.value)
elif isinstance(self.value, RichText):
# Ensure RichTextBlock raw value always returns the "internal format", rather than the conterted value
# as per https://docs.wagtail.io/en/stable/extending/rich_text_internals.html#data-format.
# Note that RichTextBlock.value will be rendered HTML by default.
return self.value.source

return self.value
Loading
Loading