Source code for sphinx_hosting.wildewidgets.search

from typing import cast

import humanize
from django.db.models import Model
from django.urls import reverse
from django.utils import timezone
from django.utils.html import strip_tags
from haystack.models import SearchResult
from haystack.query import SearchQuerySet
from wildewidgets import (
    Block,
    Column,
    CrispyFormWidget,
    FontIcon,
    HorizontalLayoutBlock,
    Link,
    LinkButton,
    PagedModelWidget,
    Row,
    TagBlock,
)

from ..forms import GlobalSearchForm
from ..models import Classifier, Project, SphinxPage


[docs]class GlobalSearchFormWidget(CrispyFormWidget): """ Encapsulates the :py:class:`sphinx_hosting.forms.GlobalSearchForm`. """ name: str = "global-search" css_class: str = "mb-3"
[docs] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.form is None: self.form = GlobalSearchForm()
[docs]class SearchResultBlock(Block): """ Block we use for rendering a particular search result on the search results page. Keyword Args: object: the :py:class:`haystack.models.SearchResult` object to render """ block: str = "search-result" max_text_length: int = 200
[docs] class Header(Block): name: str = "search-result__header"
[docs] def __init__(self, result: SearchResult, **kwargs): super().__init__(**kwargs) self.add_class("mb-3") now = timezone.now() ago = humanize.naturaldelta( now - result.object.version.modified, minimum_unit="seconds" ) self.add_block( HorizontalLayoutBlock( Block( str(result.object.version), name="search-result__header__subtitle", css_class="fs-6 text-muted font-bold", ), Block( f"{ago} ago", name="search-result__header__ago", css_class="text-muted fs-6 text-uppercase", ), justify="between", align="baseline", ) ) self.add_block(Block(result.object.title, tag="h3"))
[docs] def __init__(self, object: SearchResult = None, **kwargs): # noqa: A002 result = cast("SearchResult", object) super().__init__(**kwargs) self.add_class("shadow") self.add_class("border") self.add_class("p-4") self.add_class("mb-4") self.add_block(SearchResultBlock.Header(result)) page = cast("SphinxPage", result.object) text = strip_tags(page.body) text = text[: self.max_text_length].rsplit(" ", 1)[0] + "..." self.add_block( Block(text, name="search-result_snippet", css_class="fs-8 text-muted mb-3") ) self.add_block( HorizontalLayoutBlock( LinkButton(text="Read", url=page.get_absolute_url()), Block(f"Rank: {result.score}", css_class="fs-6 text-muted"), justify="between", align="baseline", ) )
[docs]class SearchResultsHeader(HorizontalLayoutBlock): """ The header for the search results listing (not the page header -- that is :py:class:`SearchResultsPageHeader`). Args: results: the Haystack search queryset containing our search results """ name: str = "search-results__title" justify: str = "between"
[docs] def __init__(self, results: SearchQuerySet, **kwargs): super().__init__(**kwargs) self.add_block( Block( str(f"Search Results: {results.count()}"), css_class="fs-6 font-bold mb-3", ) )
[docs]class PagedSearchResultsBlock(PagedModelWidget): """ Paged listing of :py:class:`SearchResultBlock` entries describing our search results. Args: results: the Haystack search queryset containing our search results query: the text entered into the search form that got us to this results page Keyword Args: facets: a dictionary of facet names to lists of facet values that were selected on the search results page """ page_kwarg: str = "p" paginate_by: int = 10 model_widget: Block = SearchResultBlock
[docs] def __init__( self, results: SearchQuerySet, query: str | None, facets: dict[str, list[str]] | None = None, **kwargs, ): if query is not None: kwargs["extra_url"] = {"q": query} if facets: for key, value in facets.items(): kwargs["extra_url"][key] = ",".join(value) super().__init__(queryset=results, **kwargs)
[docs]class FacetBlock(Block): """ Base class for blocks that appear to the right of the search results listing on the search results page that allows you to refine your results by a particular facet that is present in the result set. Subclass this to create facet filtering blocks for specific facets. Any facet you want to filter by must be defined on your search index by adding ``faceted=True`` to the field definition. Args: results: the Haystack search queryset containing our search results query: the text entered into the search form that got us to this results page """ #: The model class which our facet is related to model: type[Model] #: The title for our block title: str #: The name of the facet facet: str #: The field on :py:attr:`model` that we will filter by model_field: str
[docs] def __init__(self, results: SearchQuerySet, query: str | None, **kwargs): self.query = query super().__init__(**kwargs) self.add_class("border") self.add_class("bg-white") facet_qs = results.facet(self.facet) stats = facet_qs.facet_counts() self.add_block(Block(self.title, tag="h3", css_class="p-3")) body = Block(name="list-group", css_class="list-group-flush") for identifier, count in stats["fields"][self.facet]: kwargs = {self.model_field: identifier} instance = self.model.objects.get(**kwargs) body.add_block( Block( HorizontalLayoutBlock( Link( str(instance), url=instance.get_absolute_url(), # type: ignore[attr-defined] css_class="fs-5", ), HorizontalLayoutBlock( TagBlock(count, color="cyan", css_class="me-2"), LinkButton( text="Filter", url=reverse("sphinx_hosting:search") + f"?q={query}&{self.facet}={identifier}", color="outline-secondary", size="sm", ), ), ), tag="li", name="list-group-item", ) ) self.add_block(body)
[docs]class SearchResultsClassifiersFacet(FacetBlock): """ A :py:class:`FacetBlock` that allows the user to filter search results by classifier. """ model: type[Model] = Classifier title: str = "Classifiers" facet: str = "classifiers" model_field: str = "name"
[docs]class SearchResultsProjectFacet(FacetBlock): """ A :py:class:`FacetBlock` that allows the user to filter search results by project. """ model: type[Model] = Project title: str = "Projects" facet: str = "project_id" model_field: str = "pk"
[docs]class SearchResultsPageHeader(Block): """ The header for the entire search results page. This shows the search string that got us here, and any active facet filters, allowing the user to remove any active filter by clicking the "X" next to the filter name. Args: query: the text entered into the search form that got us to this results page Keyword Args: facets: the active facet filters. This will be a dict where the key is the facet name, and the value is a list of facet values to filter by. """ block: str = "search-results__header" css_class: str = "mb-5"
[docs] def __init__( self, query: str | None, facets: dict[str, list[str]] | None = None, **kwargs, ): if facets is None: facets = {} super().__init__(**kwargs) self.add_class("mb-4") self.add_block( Block( "Search Results", tag="h1", name="search-results__header__title", ) ) self.add_block( Block( f'Query: "{query}"', name="search-results__header__subtitle", css_class="text-muted fs-6 text-uppercase", ) ) if facets: buttons = HorizontalLayoutBlock( Block("Filters:", tag="h3", css_class="me-3"), justify="start", align="baseline", css_class="mt-3", ) for facet, identifiers in facets.items(): for identifier in identifiers: if facet == "project_id": project = Project.objects.get(pk=identifier) label = Block( FontIcon(icon="file-excel-fill"), f"Project: {project.title}", ) elif facet == "classifiers": classifier = Classifier.objects.get(name=identifier) label = Block( FontIcon(icon="file-excel-fill"), f"Classifier: {classifier.name}", ) buttons.add_block( LinkButton( text=label, url=reverse("sphinx_hosting:search") + f"?q={query}", color="outline-azure", css_class="me-3", ) ) self.add_block(buttons)
[docs]class PagedSearchLayout(Block): """ The page layout for the entire search results page. Args: query: the text entered into the search form that got us to this results page Keyword Args: query: the text entered into the search form that got us to this results page facets: the active facet filters. This will be a dict where the key is the facet name, and the value is a list of facet values to filter by. """ name: str = "search-layout" modifier: str = "paged"
[docs] def __init__( self, results: SearchQuerySet, query: str | None = None, facets: dict[str, list[str]] | None = None, **kwargs, ): self.query = query if facets is None: facets = {} super().__init__(**kwargs) self.add_block(SearchResultsPageHeader(query, facets=facets)) self.add_block(SearchResultsHeader(results)) row = Row() row.add_column( Column( PagedSearchResultsBlock(results, query, facets=facets), name="middle", base_width=8, ) ) row.add_column( Column( SearchResultsProjectFacet(results, query, css_class="mb-4"), SearchResultsClassifiersFacet(results, query), name="right", base_width=4, ) ) self.add_block(row)