diff --git a/cms/dashboard/wagtail_hooks.py b/cms/dashboard/wagtail_hooks.py index 3f575675c..57d49eeb7 100644 --- a/cms/dashboard/wagtail_hooks.py +++ b/cms/dashboard/wagtail_hooks.py @@ -2,9 +2,16 @@ from django.templatetags.static import static from django.utils.html import format_html from django.utils.safestring import SafeString +from draftjs_exporter.dom import DOM from wagtail import hooks from wagtail.admin.menu import MenuItem +from wagtail.admin.rich_text.converters.html_to_contentstate import ( + ExternalLinkElementHandler, + PageLinkElementHandler, +) from wagtail.admin.site_summary import PagesSummaryItem, SummaryItem +from wagtail.models import Page +from wagtail.whitelist import check_url @hooks.register("insert_global_admin_css") @@ -82,3 +89,43 @@ def register_icons(icons: list[str]) -> list[str]: """ return icons + ADDITIONAL_CUSTOM_ICONS + + +def link_entity_with_href(props: dict): + link_props = _build_link_props(props=props) + return DOM.create_element("a", link_props, props["children"]) + + +def _build_link_props(props: dict) -> dict[str, str | int]: + link_props = {} + page_id = props.get("id") + + if page_id is not None: + link_props["linktype"] = "page" + link_props["id"] = page_id + + # This is the added functionality + # on top of the original Wagtail implementation + page = Page.objects.get(id=page_id).specific + link_props["href"] = page.full_url + else: + link_props["href"] = check_url(url_string=props.get("url")) + + return link_props + + +@hooks.register("register_rich_text_features", order=1) +def register_link_props(features): + features.register_converter_rule( + "contentstate", + "link", + { + "from_database_format": { + "a[href]": ExternalLinkElementHandler("LINK"), + 'a[linktype="page"]': PageLinkElementHandler("LINK"), + }, + "to_database_format": { + "entity_decorators": {"LINK": link_entity_with_href} + }, + }, + ) diff --git a/tests/unit/cms/dashboard/test_wagtail_hooks.py b/tests/unit/cms/dashboard/test_wagtail_hooks.py index 14ccb7c6d..ac8c1d7d0 100644 --- a/tests/unit/cms/dashboard/test_wagtail_hooks.py +++ b/tests/unit/cms/dashboard/test_wagtail_hooks.py @@ -1,8 +1,16 @@ from unittest import mock +import pytest +from draftjs_exporter.dom import DOM +from wagtail.admin.rich_text.converters.html_to_contentstate import ( + ExternalLinkElementHandler, + PageLinkElementHandler, +) from wagtail.admin.site_summary import SummaryItem +from wagtail.models import Page from cms.dashboard import wagtail_hooks +from cms.dashboard.wagtail_hooks import _build_link_props, link_entity_with_href MODULE_PATH = "cms.dashboard.wagtail_hooks" @@ -100,3 +108,103 @@ def test_update_summary_items(): assert len(core_summary_items) == 1 assert core_summary_items[0].request == mock_request assert isinstance(core_summary_items[0], SummaryItem) + + +@mock.patch(f"{MODULE_PATH}.link_entity_with_href") +def test_register_link_props(spy_link_entity_with_href: mock.MagicMock): + """ + Given no input + When the wagtail hook `register_link_props` is called + Then the `link_entity_with_href()` function + is set on the link entity decorators + via the `register_converter_rule()` call + """ + # Given + spy_features = mock.Mock() + + # When + wagtail_hooks.register_link_props(features=spy_features) + + # Then + assert ( + spy_features.mock_calls[0][1][2]["to_database_format"]["entity_decorators"][ + "LINK" + ] + == spy_link_entity_with_href + ) + + +class TestLinkEntityWithHref: + @mock.patch.object(DOM, "create_element") + @mock.patch(f"{MODULE_PATH}._build_link_props") + def test_delegates_calls( + self, + spy_build_link_props: mock.MagicMock, + spy_dom_create_element: mock.MagicMock, + ): + """ + Given props containing a URL and children elements + When `link_entity_with_href()` is called + Then the call is delegated + to `_build_link_props()` to make the initial props + which are then passed to `DOM.create_element()` + """ + # Given + mocked_children = mock.Mock() + fake_props = {"url": "https://abc.com", "children": mocked_children} + + # When + link_entity_with_href(props=fake_props) + + # Then + spy_build_link_props.assert_called_once_with(props=fake_props) + spy_dom_create_element.assert_called_once_with( + "a", spy_build_link_props.return_value, mocked_children + ) + + +class TestBuildLinkProps: + @mock.patch.object(Page, "objects") + def test_build_link_props_with_valid_page_id( + self, mocked_page_model_manager: mock.MagicMock + ): + """ + Given a valid ID for a `Page` object + When `_build_link_props()` is called + Then the returned props + also contain the page full URL + """ + # Given + page_id = 1 + expected_url = "https://test-ukhsa-dashboard.com/covid-19" + mocked_page = mock.Mock() + mocked_page.specific.full_url = expected_url + mocked_page_model_manager.get.return_value = mocked_page + + # When + link_props = _build_link_props({"id": page_id}) + + # Then + mocked_page_model_manager.get.assert_called_once_with(id=page_id) + expected_props = {"linktype": "page", "id": page_id, "href": expected_url} + assert link_props == expected_props + + @mock.patch(f"{MODULE_PATH}.check_url") + def test_build_link_props_with_url(self, spy_check_url: mock.MagicMock): + """ + Given a URL for a `Page` object + When `_build_link_props()` is called + Then the returned props + contain only the page URL + """ + # Given + url = "https://test-ukhsa-dashboard.com/covid-19" + spy_check_url.return_value = url + + # When + link_props = _build_link_props(props={"url": url}) + + # Then the correct link properties are returned + expected_props = {"href": url} + assert link_props == expected_props + spy_check_url.assert_called_once_with(url_string=url)