Source code for sphinx_hosting.wildewidgets.sphinx_page

from typing import Any

from crequest.middleware import CrequestMiddleware
from django.template import Context, Template
from wildewidgets import (
    Block,
    CardHeader,
    CardWidget,
    Column,
    FontIcon,
    HTMLWidget,
    Link,
    LinkButton,
    Menu,
    MenuItem,
    Row,
    TwoColumnLayout,
)

from ..models import SphinxPage, Version

# ------------------------------------------------------
# SphinxPage related widgets
# ------------------------------------------------------


[docs]class SphinxPagePagination(Row): """ Draws the "Previous Page", Parent Page and Next Page buttons that are found at the top of each :py:class:`sphinx_hosting.views.SphinxPageDetailView`. It is built out of a Tabler/Bootstrap ``row``, with each of the buttons in an equal sized ``col``. """ name: str = "sphinxpage-pagination"
[docs] def __init__(self, page: SphinxPage, **kwargs): super().__init__(**kwargs) self.add_column( Column(name="left", alignment="start", viewport_widths={"md": "4"}) ) self.add_column( Column(name="center", alignment="center", viewport_widths={"md": "4"}) ) self.add_column( Column(name="right", alignment="end", viewport_widths={"md": "4"}) ) if hasattr(page, "previous_page") and page.previous_page.first(): self.add_to_left( LinkButton( text=Block( FontIcon("box-arrow-in-left"), page.previous_page.first().title ), url=page.previous_page.first().get_absolute_url(), name=f"{self.name}__previous", css_class="bg-azure-lt", ) ) if page.parent: self.add_to_center( LinkButton( text=Block(FontIcon("box-arrow-in-up"), page.parent.title), # type: ignore[attr-defined] url=page.parent.get_absolute_url(), # type: ignore[attr-defined] name=f"{self.name}__parent", css_class="bg-azure-lt", ) ) if page.next_page: self.add_to_right( LinkButton( text=Block(page.next_page.title, FontIcon("box-arrow-in-right")), # type: ignore[attr-defined] url=page.next_page.get_absolute_url(), # type: ignore[attr-defined] name=f"{self.name}__next", css_class="bg-azure-lt", ) )
[docs]class SphinxPagePermalinkWidget(CardWidget): """ Draws the "Permalink" button that is found at the top of the right-hand column of each :py:class:`sphinx_hosting.views.SphinxPageDetailView`. Args: page: the ``SphinxPage`` we are rendering """
[docs] def __init__(self, page: SphinxPage, **kwargs): super().__init__(**kwargs) self.page = page self.widget = Block( # This is the button that will be clicked to copy the permalink to the # clipboard. Link( FontIcon("share", css_class="me-2"), "Permalink", css_id="page-permalink", css_class="btn btn-outline-primary ms-auto", ), # This is the alert that will be displayed when the permalink is # successfully copied to the clipboard. It starts hidden. Block( "Permalink copied to clipboard!", css_id="permalink-success-alert", css_class="alert alert-success mt-2", attributes={"role": "alert", "style": "display: none !important;"}, ), css_class="d-flex flex-column", )
[docs] def get_script(self) -> str | None: """ Return the Javascript that will be executed when the "Permalink" button is clicked. This will copy the permalink to the browser clipboard. Returns: The javascript for this block. """ request = CrequestMiddleware.get_request() host = request.get_host() # nosemgrep: python.flask.security.audit.directly-returned-format-string.directly-returned-format-string # noqa: E501, ERA001 return f""" $('#page-permalink').click(function() {{ navigator.clipboard.writeText("https://{host}{self.page.get_permalink()}").then( () => {{ $('#permalink-success-alert').show("slow"); $('#permalink-success-alert').delay(3000).hide("slow"); }}, () => {{ alert("Copy failed!"); }} ); }}); """
[docs]class SphinxPageBodyWidget(CardWidget): """ The body of the page. The body as stored in the model is actually a Django template, so we retrieve the body, run it through the Django template engine, and display the results. Args: page: the ``SphinxPage`` we are rendering """ css_class: str = "sphinxpage-body"
[docs] def __init__(self, page: SphinxPage, **kwargs): super().__init__(**kwargs) body = "{% load sphinx_hosting %}\n" + page.body self.widget = HTMLWidget(html=Template(body).render(Context()))
[docs]class SphinxPageTableOfContentsWidget(CardWidget): """ Draws the in-page navigation -- the header hierarchy. Args: page: the ``SphinxPage`` we are rendering """ css_class: str = "sphinxpage-toc"
[docs] def __init__(self, page: SphinxPage, **kwargs): super().__init__(**kwargs) self.widget = HTMLWidget(html=page.local_toc) self.set_header( CardHeader(header_level=3, header_text="Table of Contents", css_class="") )
[docs]class SphinxPageGlobalTableOfContentsMenu(Menu): """ The version-specific navigation menu that gets inserted into the page sidebar when viewing the documentation for a :py:class:`sphinx_hosting.models.Version`. It will appear on all pages for that version. """ css_class: str = "mt-4" title_css_classes: str = "mt-3"
[docs] @classmethod def parse_obj(cls, version: Version) -> "SphinxPageGlobalTableOfContentsMenu": """ Parse the globaltoc of a :py:class:`sphinx_hosting.models.Version` into a :py:class:`wildewidgets.Menu` suitable for insertion into a :py:class:`wildewidgets.Navbar` The :py:attr:`sphinx_hosting.models.Version.globaltoc` is a dict that looks like this:: { items: [ {'text': 'foo'}, {'text': 'bar', 'url': '/foo', 'icon': 'blah'} {'text': 'bar', 'url': '/foo', 'icon': 'blah', items: [{'text': 'blah' ...} ...]} ... ] } Args: version: the ``Version`` for which we are building the menu Returns: A configured ``SphinxPageGlobalTableOfContentsMenu``. """ # noqa: E501 data = version.globaltoc menu_items = cls._load_menuitems(data["items"]) if version.project.related_links.exists(): # type: ignore[attr-defined] link_items: list[MenuItem] = [MenuItem(text="Related Links")] for link in version.project.related_links.all(): # type: ignore[attr-defined] link_items.append(MenuItem(text=link.title, url=link.uri, icon="link")) # noqa: PERF401 if len(menu_items) == 1: # There's only a single page in this version, so we can # just extend the list of menu items with the link items menu_items.extend(link_items) else: if menu_items[1].url is not None and menu_items[1].items is not None: # Insert a "Content" heading after the "Home" link # to separate it from the links menu_items.insert(1, MenuItem(text="Content")) link_items.reverse() for item in link_items: menu_items.insert(1, item) if menu_items[0].text != "Home": # Let's be consistent about naming the top page of the # version "Home". The globaltoc adds a Home link if there # isn't one, but those without globaltoc will have their # top page named after the project. menu_items[0].text = "Home" return cls(*menu_items)
@classmethod def _load_menuitems(cls, items: list[dict[str, Any]]) -> list[MenuItem]: """ Given a list like this:: [ {'text': 'foo'}, {'text': 'bar', 'url': '/foo', 'icon': 'blah'} {'text': 'bar', 'url': '/foo', 'icon': 'blah', items: [{'text': 'blah' ...} ...]} ... ] Return a list of :py:class:`wildewidgets.MenuItem` objects loaded from that data. Returns: A list of :py:class:`wildewidgets.MenuItem` objects. """ # noqa: E501 menu_items: list[MenuItem] = [] for item in items: if "items" in item: sub_items = cls._load_menuitems(item["items"]) menu_items.append( MenuItem( text=item["text"], url=item.get("url", None), icon=item.get("icon", None), items=sub_items, ) ) else: menu_items.append(MenuItem(**item)) return menu_items
[docs]class SphinxPageTitle(Block): """ The title block for a :py:class:`sphinx_hosting.models.SphinxPage` page. Args: page: the ``SphinxPage`` to render """ block: str = "sphinxpage-title" css_class: str = "mb-5"
[docs] def __init__(self, page: SphinxPage, **kwargs): super().__init__(**kwargs) self.project_title = page.version.project.title # type: ignore[attr-defined] self.version_number = page.version.version # type: ignore[attr-defined] self.title = page.title self.add_block( Block( f"{self.project_title}-{self.version_number}", name="project-title", css_class="text-muted fs-6 text-uppercase", ) ) self.add_block( Block( self.title, tag="h1", ) )
[docs]class SphinxPageLayout(Block): """ The page layout for a single :py:class:`sphinx_hosting.models.SphinxPage`. It consists of a two column layout with the page's table of contents in the left column, and the content of the page in the right column. Args: page: the ``SphinxPage`` to render """ left_column_width: int = 8
[docs] def __init__(self, page: SphinxPage, **kwargs): super().__init__(**kwargs) self.add_block(SphinxPagePagination(page, css_class="mb-4")) self.add_block(SphinxPageTitle(page)) layout = TwoColumnLayout(left_column_width=self.left_column_width) layout.add_to_right(SphinxPagePermalinkWidget(page)) if page.local_toc: layout.add_to_right(SphinxPageTableOfContentsWidget(page)) layout.add_to_left(SphinxPageBodyWidget(page)) self.add_block(layout) self.add_block(SphinxPagePagination(page, css_class="mt-5"))