From 53d1ca1526313ed147ad78101ed9f66e31945ff7 Mon Sep 17 00:00:00 2001 From: Paul Schilling Date: Tue, 1 Aug 2023 12:50:14 +0200 Subject: [PATCH] [#1535] add subheading anchors to product page sidebar --- .../components/AnchorMenu/AnchorMenu.html | 20 ++++++++++++++ src/open_inwoner/pdc/tests/test_product.py | 23 ++++++++++++++++ src/open_inwoner/pdc/utils.py | 24 +++++++++++++++++ src/open_inwoner/pdc/views.py | 11 +++----- .../scss/components/Header/AnchorMenu.scss | 26 ++++++++++++++++++- src/open_inwoner/utils/ckeditor.py | 4 +++ 6 files changed, 100 insertions(+), 8 deletions(-) diff --git a/src/open_inwoner/components/templates/components/AnchorMenu/AnchorMenu.html b/src/open_inwoner/components/templates/components/AnchorMenu/AnchorMenu.html index 319cf32d4c..ef68ee0cc9 100644 --- a/src/open_inwoner/components/templates/components/AnchorMenu/AnchorMenu.html +++ b/src/open_inwoner/components/templates/components/AnchorMenu/AnchorMenu.html @@ -8,6 +8,16 @@ {% for anchor in anchors %}
  • {{ anchor.1 }} + + {% if anchor.2 %} + + {% endif %}
  • {% endfor %} {{ contents }} @@ -20,6 +30,16 @@ {% for anchor in anchors %}
  • {{ anchor.1 }} + + {% if anchor.2 %} + + {% endif %}
  • {% endfor %} {{ contents }} diff --git a/src/open_inwoner/pdc/tests/test_product.py b/src/open_inwoner/pdc/tests/test_product.py index 8d3cb32311..f4903b4299 100644 --- a/src/open_inwoner/pdc/tests/test_product.py +++ b/src/open_inwoner/pdc/tests/test_product.py @@ -254,3 +254,26 @@ def test_sidemenu_button_is_rendered_when_no_cta_inside_product_content(self): self.assertTrue(sidemenu_cta_button) self.assertIn(product.link, sidemenu_cta_button[0].values()) + + +@override_settings(ROOT_URLCONF="open_inwoner.cms.tests.urls") +class TestProductDetailView(WebTest): + def test_subheadings_in_sidebar(self): + product = ProductFactory( + content="##First subheading\rlorem ipsum...\r##Second subheading", + link="http://www.example.com", + ) + + response = self.app.get( + reverse("products:product_detail", kwargs={"slug": product.slug}) + ) + + links = response.pyquery(".anchor-menu__sublist").find("a") + + self.assertEqual(len(links), 2) + + self.assertEqual(links[0].text, "First subheading") + self.assertEqual(links[0].attrib["href"], "#first-subheading") + + self.assertEqual(links[1].text, "Second subheading") + self.assertEqual(links[1].attrib["href"], "#second-subheading") diff --git a/src/open_inwoner/pdc/utils.py b/src/open_inwoner/pdc/utils.py index 33785cbe50..1e5c350733 100644 --- a/src/open_inwoner/pdc/utils.py +++ b/src/open_inwoner/pdc/utils.py @@ -1 +1,25 @@ +from django.utils.text import slugify + +import markdown +from bs4 import BeautifulSoup + PRODUCT_PATH_NAME = "products" + + +def extract_subheadings(content: str, tag: str) -> list[tuple[str, str]]: + """ + :returns: a list of tuples containing a subheading (the text of the `tag` element) + and a slug for the corresponding HTML anchor + """ + md = markdown.Markdown() + html_string = md.convert(content) + + soup = BeautifulSoup(html_string, "html.parser") + + subs = [] + for tag in soup.find_all("h2"): + subheading = tag.text + slug = slugify(subheading) + subs.append((subheading, slug)) + + return subs diff --git a/src/open_inwoner/pdc/views.py b/src/open_inwoner/pdc/views.py index c64897f80a..04f06def51 100644 --- a/src/open_inwoner/pdc/views.py +++ b/src/open_inwoner/pdc/views.py @@ -9,13 +9,13 @@ from open_inwoner.configurations.models import SiteConfiguration from open_inwoner.pdc.models.product import ProductCondition -from open_inwoner.plans.models import Plan from open_inwoner.questionnaire.models import QuestionnaireStep from ..utils.views import CommonPageMixin from .choices import YesNo from .forms import ProductFinderForm from .models import Category, Product, ProductLocation, Question +from .utils import extract_subheadings class CategoryBreadcrumbMixin: @@ -185,8 +185,10 @@ def get_context_data(self, **kwargs): product = self.get_object() context = super().get_context_data(**kwargs) + subheadings = extract_subheadings(product.content, tag="h2") + anchors = [ - ("#title", product.name), + ("#title", product.name, subheadings), ] if product.question_set.exists(): anchors.append(("#faq", _("Veelgestelde vragen"))) @@ -194,13 +196,8 @@ def get_context_data(self, **kwargs): anchors.append(("#files", _("Bestanden"))) if product.locations.exists(): anchors.append(("#locations", _("Locaties"))) - if product.links.exists(): - anchors.append(("#links", _("Links"))) if product.contacts.exists(): anchors.append(("#contact", _("Contact"))) - if product.related_products.published().exists(): - anchors.append(("#see", _("Zie ook"))) - # anchors.append(("#share", _("Delen"))) disabled for #822 context["anchors"] = anchors context["related_products_start"] = 6 if product.links.exists() else 1 diff --git a/src/open_inwoner/scss/components/Header/AnchorMenu.scss b/src/open_inwoner/scss/components/Header/AnchorMenu.scss index 2a5d4f1ca1..451f25b00a 100644 --- a/src/open_inwoner/scss/components/Header/AnchorMenu.scss +++ b/src/open_inwoner/scss/components/Header/AnchorMenu.scss @@ -63,7 +63,7 @@ margin-left: var(--spacing-large); } - .link { + &__list-item { box-sizing: border-box; padding: var(--spacing-large) var(--spacing-medium); @@ -82,6 +82,30 @@ } } + // nested list + &__sublist { + list-style-type: '- '; + padding-left: var(--spacing-large); + padding-top: var(--spacing-large); + line-height: var(--spacing-large); + + li { + border-left: none; + } + li:not(:last-child) { + margin-bottom: var(--spacing-large); + } + li > a { + font-size: small; + } + + &--mobile { + li > a { + font-size: var(--font-size-heading-4) !important; + } + } + } + &--mobile__title { box-sizing: border-box; padding: var(--spacing-large) var(--spacing-large) var(--spacing-medium) diff --git a/src/open_inwoner/utils/ckeditor.py b/src/open_inwoner/utils/ckeditor.py index 2b0406bb97..16c117cbb0 100644 --- a/src/open_inwoner/utils/ckeditor.py +++ b/src/open_inwoner/utils/ckeditor.py @@ -1,3 +1,4 @@ +from django.utils.text import slugify from django.utils.translation import gettext as _ import markdown @@ -52,6 +53,9 @@ def get_product_rendered_content(product): element.attrs["class"] = class_name + if tag == "h2": + element.attrs["id"] = slugify(element.text) + if "[CTABUTTON]" in element.text: # decompose the element when product doesn't have either a link or a form if not (product.link or product.form):