From 25169f1703dbb74bed0cd8abdb9b9047cfdf10a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zden=C4=9Bk=20B=C3=B6hm?= Date: Thu, 27 Nov 2025 09:36:16 +0100 Subject: [PATCH 1/3] Add the set_html_id template tag to resolve the random html.id attribute. --- .../djangocms_frontend/bootstrap5/accordion.html | 2 +- .../bootstrap5/accordion_item.html | 10 +++++----- .../bootstrap5/collapse-container.html | 2 +- .../djangocms_frontend/bootstrap5/collapse.html | 2 +- djangocms_frontend/models.py | 4 +--- djangocms_frontend/templatetags/frontend.py | 12 ++++++++++++ 6 files changed, 21 insertions(+), 11 deletions(-) diff --git a/djangocms_frontend/contrib/accordion/templates/djangocms_frontend/bootstrap5/accordion.html b/djangocms_frontend/contrib/accordion/templates/djangocms_frontend/bootstrap5/accordion.html index da1c5802..0b48b937 100644 --- a/djangocms_frontend/contrib/accordion/templates/djangocms_frontend/bootstrap5/accordion.html +++ b/djangocms_frontend/contrib/accordion/templates/djangocms_frontend/bootstrap5/accordion.html @@ -1,5 +1,5 @@ {% load cms_tags frontend %} -<{{ instance.tag_type }}{{ instance.get_attributes }} id="parent-{{ instance.uuid }}"> +<{{ instance.tag_type }}{{ instance.get_attributes }} {% set_html_id instance as html_id %}id="parent-{{ html_id }}"> {% for plugin in instance.child_plugin_instances %} {% with parentloop=forloop parent=instance %}{% render_plugin plugin %}{% endwith %} {% empty %}{% user_message _("Add accordion items here") %} diff --git a/djangocms_frontend/contrib/accordion/templates/djangocms_frontend/bootstrap5/accordion_item.html b/djangocms_frontend/contrib/accordion/templates/djangocms_frontend/bootstrap5/accordion_item.html index f2ce4994..58136b74 100644 --- a/djangocms_frontend/contrib/accordion/templates/djangocms_frontend/bootstrap5/accordion_item.html +++ b/djangocms_frontend/contrib/accordion/templates/djangocms_frontend/bootstrap5/accordion_item.html @@ -1,16 +1,16 @@ {% load cms_tags frontend %} {% spaceless %} -
+
{% set_html_id instance as html_id %} <{{ parent.accordion_header_type|default:"h2" }} class="accordion-header" - id="heading-{{ instance.uuid }}"> + aria-controls="item-{{ instance.html_id }}">{% inline_field instance "accordion_item_header" %} - <{{ instance.tag_type }}{{ instance.get_attributes }} id="item-{{ instance.uuid }}" aria-labelledby="heading-{{ instance.uuid }}" data-bs-parent="#parent-{{ parent.uuid }}"> + <{{ instance.tag_type }}{{ instance.get_attributes }} id="item-{{ instance.html_id }}" aria-labelledby="heading-{{ instance.html_id }}" data-bs-parent="#parent-{{ parent.html_id }}">
{% endspaceless %} {% with parent=instance %} {% for plugin in instance.child_plugin_instances %} diff --git a/djangocms_frontend/contrib/collapse/templates/djangocms_frontend/bootstrap5/collapse-container.html b/djangocms_frontend/contrib/collapse/templates/djangocms_frontend/bootstrap5/collapse-container.html index 7c2d2e3a..9f11a3bd 100644 --- a/djangocms_frontend/contrib/collapse/templates/djangocms_frontend/bootstrap5/collapse-container.html +++ b/djangocms_frontend/contrib/collapse/templates/djangocms_frontend/bootstrap5/collapse-container.html @@ -2,7 +2,7 @@ <{{ instance.tag_type }}{{ instance.get_attributes }} id="{{ instance.container_identifier }}" role="tabpanel" - data-bs-parent="#collapse-{{ parent.uuid }}" + data-bs-parent="#collapse-{{ parent.html_id }}" aria-labelledby="trigger-{{ instance.container_identifier }}"> {% for plugin in instance.child_plugin_instances %} {% with forloop as parentloop %}{% render_plugin plugin %}{% endwith %} diff --git a/djangocms_frontend/contrib/collapse/templates/djangocms_frontend/bootstrap5/collapse.html b/djangocms_frontend/contrib/collapse/templates/djangocms_frontend/bootstrap5/collapse.html index c60ed850..d095971f 100644 --- a/djangocms_frontend/contrib/collapse/templates/djangocms_frontend/bootstrap5/collapse.html +++ b/djangocms_frontend/contrib/collapse/templates/djangocms_frontend/bootstrap5/collapse.html @@ -1,6 +1,6 @@ {% load cms_tags frontend %} <{{ instance.tag_type }}{{ instance.get_attributes }} -id="collapse-{{ instance.uuid }}" +{% set_html_id instance as html_id %}id="collapse-{{ html_id }}" data-bs-children="{{ instance.collapse_siblings }}" role="tablist" > diff --git a/djangocms_frontend/models.py b/djangocms_frontend/models.py index 3ddc6048..80ae818c 100644 --- a/djangocms_frontend/models.py +++ b/djangocms_frontend/models.py @@ -1,5 +1,3 @@ -import uuid - from cms.models import CMSPlugin from django.core.serializers.json import DjangoJSONEncoder from django.db import models @@ -48,7 +46,7 @@ class Meta: def __init__(self, *args, **kwargs): self._additional_classes = [] - self.uuid = str(uuid.uuid4()) + self.html_id = None # HTML id attribute will be set in template tag set_html_id. super().__init__(*args, **kwargs) def __getattr__(self, item): diff --git a/djangocms_frontend/templatetags/frontend.py b/djangocms_frontend/templatetags/frontend.py index 1fd0677b..d7b06617 100644 --- a/djangocms_frontend/templatetags/frontend.py +++ b/djangocms_frontend/templatetags/frontend.py @@ -20,6 +20,7 @@ from djangocms_frontend import settings from djangocms_frontend.fields import HTMLsanitized from djangocms_frontend.helpers import get_related_object as related_object +from djangocms_frontend.models import FrontendUIItem register = template.Library() @@ -73,6 +74,17 @@ def get_attributes(attribute_field, *add_classes): return mark_safe(" ".join(attrs)) +@register.simple_tag(takes_context=True) +def set_html_id(context: template.Context, instance: FrontendUIItem) -> str: + if instance.html_id is None: + request = context["request"] + key = "frontend_plugins_counter" + counter = getattr(request, key, 0) + 1 + instance.html_id = f"frontend-plugins-{counter}" + setattr(request, key, counter) + return instance.html_id + + @register.filter def get_related_object(reference): return related_object(dict(obj=reference), "obj") From f869214e4db5df5914a0f7b25bd44355d3d01a39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zden=C4=9Bk=20B=C3=B6hm?= Date: Thu, 27 Nov 2025 10:54:56 +0100 Subject: [PATCH 2/3] Add test_collapse_html_id into collapse/test_plugins.py. --- tests/collapse/test_plugins.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/collapse/test_plugins.py b/tests/collapse/test_plugins.py index 1633c68b..f04f710d 100644 --- a/tests/collapse/test_plugins.py +++ b/tests/collapse/test_plugins.py @@ -75,3 +75,27 @@ def test_collapse_container_plugin(self): self.assertEqual(response.status_code, 200) self.assertContains(response, 'aria-labelledby="trigger-10"') self.assertContains(response, "10") + + def test_collapse_html_id(self): + def create_plugin(): + plugin = add_plugin( + placeholder=self.placeholder, + plugin_type=CollapsePlugin.__name__, + language=self.language, + ) + plugin.initialize_from_form(CollapseForm).save() + self.publish(self.page, self.language) + + create_plugin() + create_plugin() + with self.login_user_context(self.superuser): + response = self.client.get(self.request_url) + self.assertEqual(response.status_code, 200) + self.assertContains( + response, + """
""", + html=True) + self.assertContains( + response, + """
""", + html=True) From d0cf7018b14a4c2e1a236a5d3a59c8b22d8454cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zden=C4=9Bk=20B=C3=B6hm?= Date: Thu, 27 Nov 2025 11:27:49 +0100 Subject: [PATCH 3/3] Better accessing to context request. --- djangocms_frontend/templatetags/frontend.py | 15 ++++++++++----- tests/collapse/test_plugins.py | 14 ++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/djangocms_frontend/templatetags/frontend.py b/djangocms_frontend/templatetags/frontend.py index d7b06617..2d0057fb 100644 --- a/djangocms_frontend/templatetags/frontend.py +++ b/djangocms_frontend/templatetags/frontend.py @@ -1,5 +1,6 @@ import json import typing +import uuid from classytags.arguments import Argument, MultiKeywordArgument from classytags.core import Options, Tag @@ -11,6 +12,7 @@ from django.contrib.contenttypes.models import ContentType from django.core.serializers.json import DjangoJSONEncoder from django.db import models +from django.http import HttpRequest from django.template.defaultfilters import safe from django.utils.encoding import force_str from django.utils.functional import Promise @@ -77,11 +79,14 @@ def get_attributes(attribute_field, *add_classes): @register.simple_tag(takes_context=True) def set_html_id(context: template.Context, instance: FrontendUIItem) -> str: if instance.html_id is None: - request = context["request"] - key = "frontend_plugins_counter" - counter = getattr(request, key, 0) + 1 - instance.html_id = f"frontend-plugins-{counter}" - setattr(request, key, counter) + request = context.get("request") + if isinstance(request, HttpRequest): + key = "frontend_plugins_counter" + counter = getattr(request, key, 0) + 1 + instance.html_id = f"frontend-plugins-{counter}" + setattr(request, key, counter) + else: + instance.html_id = f"uuid4={uuid.uuid4()}" return instance.html_id diff --git a/tests/collapse/test_plugins.py b/tests/collapse/test_plugins.py index f04f710d..425f2db1 100644 --- a/tests/collapse/test_plugins.py +++ b/tests/collapse/test_plugins.py @@ -1,5 +1,8 @@ +from unittest.mock import patch + from cms.api import add_plugin from cms.test_utils.testcases import CMSTestCase +from django.template import Context from djangocms_frontend.contrib.collapse.cms_plugins import ( CollapseContainerPlugin, @@ -11,6 +14,8 @@ CollapseForm, CollapseTriggerForm, ) +from djangocms_frontend.models import FrontendUIItem +from djangocms_frontend.templatetags.frontend import set_html_id from ..fixtures import TestFixture @@ -99,3 +104,12 @@ def create_plugin(): response, """
""", html=True) + + def test_set_html_id(self): + instance = FrontendUIItem() + context = Context() + with patch("os.urandom", lambda n: b'\x1bB\x96\xabyI\xf6`\xd0\xc0,\xf8\x83\xe8,\xb8'): + html_id = set_html_id(context, instance) + identifier = "uuid4=1b4296ab-7949-4660-90c0-2cf883e82cb8" + self.assertEqual(html_id, identifier) + self.assertEqual(instance.html_id, identifier)