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"))