Skip to content

Commit

Permalink
Fix circular import for PageInterface when using custom page interf…
Browse files Browse the repository at this point in the history
…ace (#404)

* Fix circular import for `PageInterface`
* Update changelog
* Fix artifact upload in CI
   https://github.blog/changelog/2024-08-19-notice-of-upcoming-deprecations-and-breaking-changes-in-github-actions-runners/
* Test with page interface inheriting from default
* Fix coverage upload for Postgres tests
* Extract `StreamFieldInterface` to `interfaces.py`
* Fix formatting
  • Loading branch information
mgax authored Sep 17, 2024
1 parent d9cd8f2 commit db86c97
Show file tree
Hide file tree
Showing 15 changed files with 273 additions and 247 deletions.
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

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

0 comments on commit db86c97

Please sign in to comment.