Skip to content

Commit

Permalink
WIP: feat: add search_fields argument
Browse files Browse the repository at this point in the history
allow for more specific search queries by specifying the search_fields.

TODO:
My searchFields tests don't seem to be working quite the way I'd like. I tried setting up a body field to specify alongside title, but it didn't work. At first I didn't realize why.  In my customized grapple implementation, I use my own custom page type as the root for all queries rather the wagtail.model.Page that has all my common search_fields...  I'm realizing that doesn't work with grapple atm as you can't specify an alternate root model.   Where grapple uses a [mixin with qs=WagtailPageObjects.all()](https://github.com/torchbox/wagtail-grapple/blob/0d20a8cd8de6cceb260a1ade564effa6c2db1e79/grapple/types/pages.py#L377),  I use standalone function that looks like
```
def _resolve_search_pages(
    info,
    content_types=[],
    page=1,
    per_page=10,
    root=TnPage.objects.live().specific(),
    categories=[],
    tags=[],
    ancestor=None,
    parent=None,
    in_menu=None,
    **kwargs
):
    """
    root allows an alternative queryset to be passed in to allow
    for inheriting types to filter the queryset first.
    """
```

I need to resolve this issue before this will be usable to end users... possible solutions...

1. pass a 'root' into PagesQuery to overide the base model for pages and add a way to specify the root model.
2. find a way to get this to work with .get_specific_page

I don't have any other ideas at the moment.
  • Loading branch information
dopry committed Sep 20, 2024
1 parent 7463002 commit 64475f2
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 0 deletions.
24 changes: 24 additions & 0 deletions grapple/types/structures.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class QuerySetList(graphene.List):
* ``offset``
* ``search_query``
* ``search_operator``
* ``search_fields``
* ``order``
:param enable_in_menu: Enable in_menu filter.
Expand All @@ -58,6 +59,8 @@ class QuerySetList(graphene.List):
:type enable_offset: bool
:param enable_search: Enable search query argument.
:type enable_search: bool
:param enable_search_fields: Enable search fields argument, enable_search must also be True
:type enable_search_fields: bool
:param enable_search_operator: Enable search operator argument, enable_search must also be True
:type enable_search_operator: bool
:param enable_order: Enable ordering via query argument.
Expand All @@ -70,6 +73,7 @@ def __init__(self, of_type, *args, **kwargs):
enable_offset = kwargs.pop("enable_offset", True)
enable_order = kwargs.pop("enable_order", True)
enable_search = kwargs.pop("enable_search", True)
enable_search_fields = kwargs.pop("enable_search_fields", True)
enable_search_operator = kwargs.pop("enable_search_operator", True)

# Check if the type is a Django model type. Do not perform the
Expand Down Expand Up @@ -134,6 +138,14 @@ def __init__(self, of_type, *args, **kwargs):
default_value="and",
)

if enable_search_fields:
kwargs["search_fields"] = graphene.Argument(
graphene.List(graphene.String),
description=_(
"A list of fields to search in. see: https://docs.wagtail.org/en/stable/topics/search/searching.html#specifying-the-fields-to-search"
),
)

if "id" not in kwargs:
kwargs["id"] = graphene.Argument(graphene.ID, description=_("Filter by ID"))

Expand Down Expand Up @@ -187,10 +199,13 @@ def PaginatedQuerySet(of_type, type_class, **kwargs):
* ``per_page``
* ``search_query``
* ``search_operator``
* ``search_fields``
* ``order``
:param enable_search: Enable search query argument.
:type enable_search: bool
:param enable_search_fields: Enable search fields argument, enable_search must also be True
:type enable_search_fields: bool
:param enable_search_operator: Enable search operator argument, enable_search must also be True
:type enable_search_operator: bool
:param enable_order: Enable ordering via query argument.
Expand All @@ -199,6 +214,7 @@ def PaginatedQuerySet(of_type, type_class, **kwargs):

enable_in_menu = kwargs.pop("enable_in_menu", False)
enable_search = kwargs.pop("enable_search", True)
enable_search_fields = kwargs.pop("enable_search_fields", True)
enable_search_operator = kwargs.pop("enable_search_operator", True)
enable_order = kwargs.pop("enable_order", True)
required = kwargs.get("required", False)
Expand Down Expand Up @@ -265,6 +281,14 @@ def PaginatedQuerySet(of_type, type_class, **kwargs):
default_value="and",
)

if enable_search_fields:
kwargs["search_fields"] = graphene.Argument(
graphene.List(graphene.String),
description=_(
"A comma-separated list of fields to search in. see: https://docs.wagtail.org/en/stable/topics/search/searching.html#specifying-the-fields-to-search"
),
)

if "id" not in kwargs:
kwargs["id"] = graphene.Argument(graphene.ID, description=_("Filter by ID"))

Expand Down
16 changes: 16 additions & 0 deletions grapple/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ def resolve_queryset(
collection=None,
in_menu=None,
search_operator="and",
search_fields=None,
**kwargs,
):
"""
Expand All @@ -127,6 +128,8 @@ def resolve_queryset(
:param search_operator: The operator to use when combining search terms.
Defaults to "and".
:type search_operator: "and" | "or"
:param search_fields: A list of fields to search. Defaults to all fields.
:type search_fields: list
"""

qs = qs.all() if id is None else qs.filter(pk=id)
Expand Down Expand Up @@ -159,10 +162,15 @@ def resolve_queryset(

filters, parsed_query = parse_query_string(search_query, str(search_operator))

# check if search_fields is provided in the query string if it isn't provided as a graphQL argument
if search_fields is None:
search_fields = filters.getlist("fields", None)

qs = qs.search(
parsed_query,
order_by_relevance=order_by_relevance,
operator=search_operator,
fields=search_fields,
)
if connection.vendor != "sqlite":
qs = qs.annotate_score("search_score")
Expand Down Expand Up @@ -212,6 +220,7 @@ def resolve_paginated_queryset(
order=None,
search_query=None,
search_operator="and",
search_fields=None,
**kwargs,
):
"""
Expand All @@ -234,6 +243,8 @@ def resolve_paginated_queryset(
:param search_operator: The operator to use when combining search terms.
Defaults to "and".
:type search_operator: "and" | "or"
:param search_fields: A list of fields to search. Defaults to all fields.
:type search_fields: list
"""
page = int(page or 1)
per_page = min(
Expand All @@ -260,10 +271,15 @@ def resolve_paginated_queryset(

filters, parsed_query = parse_query_string(search_query, search_operator)

# check if search_fields is provided in the query string if it isn't provided as a graphQL argument
if search_fields is None:
search_fields = filters.getlist("fields", None)

qs = qs.search(
parsed_query,
order_by_relevance=order_by_relevance,
operator=search_operator,
fields=search_fields,
)
if connection.vendor != "sqlite":
qs = qs.annotate_score("search_score")
Expand Down
46 changes: 46 additions & 0 deletions tests/test_grapple.py
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,52 @@ def test_search_operator_or(self):
self.assertEqual(page_data[8]["title"], "Gamma Alpha")
self.assertEqual(page_data[9]["title"], "Gamma Beta")

def test_search_fields_unset(self):
query = """
query {
pages(searchQuery: "Sigma") {
title
}
}
"""
executed = self.client.execute(query)
page_data = executed["data"].get("pages")
self.assertEqual(len(page_data), 6)
self.assertEqual(page_data[0]["title"], "Alpha")
self.assertEqual(page_data[1]["title"], "Alpha Alpha")
self.assertEqual(page_data[2]["title"], "Alpha Beta")
self.assertEqual(page_data[3]["title"], "Alpha Gamma")
self.assertEqual(page_data[4]["title"], "Beta Alpha")
self.assertEqual(page_data[5]["title"], "Gamma Alpha")

def test_search_fields_graphql_arg(self):
query = """
query {
pages(searchQuery: "Sigma", searchFields: "title") {
title
}
}
"""
executed = self.client.execute(query)
page_data = executed["data"].get("pages")
self.assertEqual(len(page_data), 0)

def test_search_fields_filter(self):
query = """
query($searchQuery: String) {
pages(searchQuery: $searchQuery) {
title
searchScore
}
}
"""
executed = self.client.execute(
query,
variables={"searchQuery": "Sigma fields:title"},
)
page_data = executed["data"].get("pages")
self.assertEqual(len(page_data), 0)


class PageUrlPathTest(BaseGrappleTest):
def _query_by_path(self, path, *, in_site=False):
Expand Down
3 changes: 3 additions & 0 deletions tests/testapp/models/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
)
from wagtail.fields import RichTextField, StreamField
from wagtail.models import Orderable, Page
from wagtail.search import index
from wagtail.snippets.models import register_snippet
from wagtail_headless_preview.models import HeadlessPreviewMixin
from wagtailmedia.edit_handlers import MediaChooserPanel
Expand Down Expand Up @@ -163,6 +164,8 @@ def custom_property(self):
"author": self.author.name if self.author else "Unknown",
}

search_fields = Page.search_fields + [index.SearchField("body")]

graphql_fields = [
GraphQLString("date", required=True),
GraphQLRichText("summary"),
Expand Down

0 comments on commit 64475f2

Please sign in to comment.