diff --git a/djangocms_frontend/common/__init__.py b/djangocms_frontend/common/__init__.py index da4c271d..b97aa311 100644 --- a/djangocms_frontend/common/__init__.py +++ b/djangocms_frontend/common/__init__.py @@ -24,7 +24,7 @@ module = import_module(f"{__name__}.{settings.framework}.{module}", module) for cls in classes: globals()[cls] = getattr(module, cls) - except ModuleNotFoundError: + except ModuleNotFoundError: # pragma: no cover for cls in classes: globals()[cls] = type(cls, (object,), {}) diff --git a/djangocms_frontend/component_pool.py b/djangocms_frontend/component_pool.py index a04d3f5b..943f1cbe 100644 --- a/djangocms_frontend/component_pool.py +++ b/djangocms_frontend/component_pool.py @@ -1,7 +1,7 @@ from collections import defaultdict +from collections.abc import Iterator import importlib import os -from collections.abc import Iterator import warnings from django import forms @@ -15,16 +15,16 @@ from djangocms_frontend.component_base import CMSFrontendComponent -def find_cms_component_templates() -> list[tuple[str, str]]: +def find_cms_component_templates(subfolder: str) -> list[tuple[str, str]]: templates = [] for app in apps.get_app_configs(): - app_template_dir = os.path.join(app.path, "templates", app.label, "cms_components") + app_template_dir = os.path.join(app.path, "templates", app.label, subfolder) if os.path.exists(app_template_dir): for root, _, files in os.walk(app_template_dir): for file in files: if file.endswith(".html") or file.endswith(".htm"): relative_path = os.path.relpath(os.path.join(root, file), app_template_dir) - templates.append((app.module.__name__, f"{app.label}/cms_components/{relative_path}")) + templates.append((app.module.__name__, f"{app.label}/{subfolder}/{relative_path}")) return templates @@ -43,7 +43,7 @@ class CMSAutoComponentDiscovery: def __init__(self, register_to): self.default_field_context.update(settings.COMPONENT_FIELDS) - templates = find_cms_component_templates() + templates = find_cms_component_templates(settings.COMPONENT_FOLDER) auto_components = self.scan_templates_for_component_declaration(templates) for component in auto_components: register_to.register(component) diff --git a/djangocms_frontend/settings.py b/djangocms_frontend/settings.py index a513e0f4..13bee5b5 100644 --- a/djangocms_frontend/settings.py +++ b/djangocms_frontend/settings.py @@ -77,7 +77,7 @@ FORM_OPTIONS = getattr(django_settings, "DJANGOCMS_FRONTEND_FORM_OPTIONS", {}) COMPONENT_FIELDS = getattr(django_settings, "DJANGOCMS_FRONTEND_COMPONENT_FIELDS", {}) - +COMPONENT_FOLDER = getattr(django_settings, "DJANGOCMS_FRONTEND_COMPONENT_FOLDER", "cms_components") framework = getattr(django_settings, "DJANGOCMS_FRONTEND_FRAMEWORK", "bootstrap5") theme = getattr(django_settings, "DJANGOCMS_FRONTEND_THEME", "djangocms_frontend") diff --git a/djangocms_frontend/templatetags/cms_component.py b/djangocms_frontend/templatetags/cms_component.py index 149cc72b..5d8869a9 100644 --- a/djangocms_frontend/templatetags/cms_component.py +++ b/djangocms_frontend/templatetags/cms_component.py @@ -61,7 +61,7 @@ def _to_tuple_if_needed(value: str) -> str | tuple[str, str]: value (str): The string to be converted. Returns: - str | tuple[str, str]: A tuple containing the two parts of the string if it contains + str | tuple[str, str]: A tuple containing the two parts of the string if it contains a delimiter, otherwise returns the original string. """ match = _TUPLE_RE.fullmatch(value) @@ -82,7 +82,7 @@ def split(value: str, delimiter: str = "|") -> list[str | tuple[str, str]]: delimiter (str, optional): The delimiter to use for splitting the string. Defaults to "|". Returns: - list[str | tuple[str, str]: A list of substrings or 2-tuples obtained by splitting the + list[str | tuple[str, str]: A list of substrings or 2-tuples obtained by splitting the input string using the delimiter. """ split_list = value.split(delimiter) diff --git a/djangocms_frontend/templatetags/frontend.py b/djangocms_frontend/templatetags/frontend.py index 1535ee08..d72f4acf 100644 --- a/djangocms_frontend/templatetags/frontend.py +++ b/djangocms_frontend/templatetags/frontend.py @@ -317,6 +317,7 @@ def render_tag(self, context, instance, attribute, **kwargs): instance = context.get("instance", None) # Use instance from context if is_registering_component(context) and attribute: + # Autodetect inline field and add it to the component update_component_properties(context, "frontend_editable_fields", attribute, append=True) elif is_inline_editing_active(context) and isinstance(instance, CMSPlugin) and instance.pk: # Only allow inline field to be rendered if inline editing is active and the instance is a CMSPlugin diff --git a/docs/source/reference.rst b/docs/source/reference.rst index e7071e67..277f1eb4 100644 --- a/docs/source/reference.rst +++ b/docs/source/reference.rst @@ -28,6 +28,24 @@ in your project's ``settings.py``. ] +.. py:attribute:: settings.DJANGOCMS_FRONTEND_COMPONENT_FOLDER + + Defaults to ``"cms_components"`` + + The subfolder where the component templates are discovered. This is used by the + :ref:`template components ` to find the templates + for the components. + + The folder needs to be created in your app's ``templates//`` directory. + If you want to use a different folder, set this to the folder name of your choice. + + For example, if you want to use ``"my_components"``, add the following line to your project's settings:: + + DJANGOCMS_FRONTEND_COMPONENT_FOLDER = "my_components" + + This causes djangocms-frontend to search for templates on the following paths: ``templates//my_components/``, + where ```` is the name of any installed app. + .. py:attribute:: settings.DJANGOCMS_FRONTEND_COMPONENT_FIELDS Defaults to ``{}`` diff --git a/docs/source/tutorial/template_components.rst b/docs/source/tutorial/template_components.rst index e37f93ac..7715389e 100644 --- a/docs/source/tutorial/template_components.rst +++ b/docs/source/tutorial/template_components.rst @@ -52,15 +52,21 @@ Hero component ============== ``djangocms-frontend`` allows developers to extend its functionality by creating -template components**. In this tutorial, we will create an **Hero component** +**template components**. In this tutorial, we will create an **Hero component** with the following fields: - ``title``: A required text field. - ``slogan``: A required text area field. - ``hero_image``: A required image field. -This component will be stored in a template directory named ``/cms_components``, -as required for ``djangocms-frontend`` template components. +This component will be stored in a template directory named ``/cms_components`` +(or any subdirectory thereof). + +.. note:: + You can change the location of your template components inside your template directory + by setting the :attr:`DJANGOCMS_FRONTEND_COMPONENT_FOLDER` setting. The default is + ``cms_components``. If you change it, you need to adjust the directory structure accordingly. + Directory Structure ------------------- diff --git a/tests/content/test_plugins.py b/tests/content/test_plugins.py index 961725c3..79491878 100644 --- a/tests/content/test_plugins.py +++ b/tests/content/test_plugins.py @@ -79,3 +79,15 @@ def test_figure_plugin(self): response = self.client.get(self.request_url) self.assertEqual(response.status_code, 200) self.assertContains(response, '
hello world

"), "hello world") + + # multiple paragraphs: kept as is + self.assertEqual(safe_caption("

hello

world

"), "

hello

world

") diff --git a/tests/test_app/templates/test_app/cms_components/ui/button.html b/tests/test_app/templates/test_app/cms_components/ui/button.html new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_app/templates/test_app/my_components/test_component.htm b/tests/test_app/templates/test_app/my_components/test_component.htm new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_autocomponent.py b/tests/test_autocomponent.py index ea0d4417..b9abf8c2 100644 --- a/tests/test_autocomponent.py +++ b/tests/test_autocomponent.py @@ -106,3 +106,20 @@ def test_multiple_cms_component_tags_error(self): invalid_template = "{% load cms_tags %}{% cms_component 'Hero' %}{% cms_component 'Footer' %}" with self.assertRaises(TemplateSyntaxError): Template(invalid_template) + + def test_component_folder_selection(self): + from djangocms_frontend.component_pool import find_cms_component_templates + + all_components = find_cms_component_templates("cms_components") + path_components = find_cms_component_templates("cms_components/ui") + private_components = find_cms_component_templates("my_components") + no_components = find_cms_component_templates("no_components") + + assert set(all_components) == { + ("tests.test_app", "test_app/cms_components/hero.html"), + ("tests.test_app", "test_app/cms_components/with_slots.html"), + ("tests.test_app", "test_app/cms_components/ui/button.html"), + } + assert path_components == [("tests.test_app", "test_app/cms_components/ui/button.html")] + assert private_components == [("tests.test_app", "test_app/my_components/test_component.htm")] + assert no_components == []