diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 50625d86..00000000 --- a/.coveragerc +++ /dev/null @@ -1,20 +0,0 @@ -[run] -branch = True -source = djangocms_frontend -omit = - djangocms_frontend/migrations/* - djangocms_frontend/management/* - */foundation6.py - tests/* - -[report] -exclude_lines = - pragma: no cover - def __repr__ - if self.debug: - if settings.DEBUG - raise AssertionError - raise NotImplementedError - if 0: - if __name__ == .__main__.: -ignore_errors = True diff --git a/.djlint_rules.yaml b/.djlint_rules.yaml deleted file mode 100644 index 5fb69a5b..00000000 --- a/.djlint_rules.yaml +++ /dev/null @@ -1,7 +0,0 @@ -- rule: - name: D034 - message: .pk or .id should only be used with "|unlocalize" - flags: re.DOTALL|re.I - patterns: - - '{{\s*[a-zA-Z0-9_.]+\.pk\s*}}' - - '{{\s*[a-zA-Z0-9_.]+\.id\s*}}' diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index fb0337cf..2778b3d5 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -32,9 +32,11 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Generate Report + - name: Intall dependencies run: | pip install -r tests/requirements/${{ matrix.requirements-file }} - coverage run run_tests.py + - name: Run coverage + run: | + coverage run -m pytest - name: Upload Coverage to Codecov uses: codecov/codecov-action@v5 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 8fda05ed..00000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Lint - -on: [push, pull_request] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - ruff: - name: ruff - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.11" - cache: 'pip' - - run: | - python -m pip install --upgrade pip - pip install ruff - - name: Run Ruff - run: ruff check djangocms_frontend diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a08f7be5..08639421 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,25 +12,27 @@ repos: rev: v3.19.0 hooks: - id: pyupgrade - args: ["--py37-plus"] + args: ["--py39-plus"] + - repo: https://github.com/adamchainz/django-upgrade rev: "1.22.1" hooks: - id: django-upgrade - args: [--target-version, "2.2"] - - repo: https://github.com/pycqa/isort - rev: 5.13.2 - hooks: - - id: isort - name: isort (python) + args: [--target-version, "4.2"] + - repo: https://github.com/PyCQA/flake8 rev: 7.1.1 hooks: - id: flake8 additional_dependencies: + - flake8-pyproject - flake8-bugbear - flake8-builtins - flake8-django - flake8-length - flake8-logging-format - - flake8-spellcheck + + - repo: https://github.com/tox-dev/pyproject-fmt + rev: v2.5.0 + hooks: + - id: pyproject-fmt diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 14248778..01d90dcc 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -82,7 +82,7 @@ Changelog * feat: Add float option for images by @fsbraun in https://github.com/django-cms/djangocms-frontend/pull/162 * feat: Add drag'n'drop support for djangocms-text-ckeditor by @fsbraun in https://github.com/django-cms/djangocms-frontend/pull/165 * fix: Ckeditor does not show icons for editing by @fsbraun in https://github.com/django-cms/djangocms-frontend/pull/163 -* fix: Replace ``stylesSet`` setting in docs with ``customConfig` for icons in ckeditor by @fsbraun in https://github.com/django-cms/djangocms-frontend/pull/164 +* fix: Replace ``stylesSet`` setting in docs with ``customConfig`` for icons in ckeditor by @fsbraun in https://github.com/django-cms/djangocms-frontend/pull/164 * ci: pre-commit autoupdate by @pre-commit-ci in https://github.com/django-cms/djangocms-frontend/pull/161 @@ -106,15 +106,15 @@ Changelog ================== * Fix incomplete migration of code plugins from djangocms-bootstrap4 -* Add compiled French locale (*.mo) +* Add compiled French locale (\*.mo) * Add partial Dutch locale 1.1.5 (2023-07-14) ================== * Fix bug where url for link select2 field was lost after app hook reload (#135) -* Use `bg-body` class on Bootstrap 5's tab navigation to support color modes (#138) -* Fix styling of icon buttons for better usager with plain django admin style (#141) +* Use ``bg-body`` class on Bootstrap 5's tab navigation to support color modes (#138) +* Fix styling of icon buttons for better usage with plain django admin style (#141) 1.1.4 (2023-05-28) ================== diff --git a/README.rst b/README.rst index ff929a91..b4c64bd3 100644 --- a/README.rst +++ b/README.rst @@ -4,29 +4,33 @@ |pypi| |docs| |coverage| |python| |django| |djangocms| |djangocms4| -**django CMS Frontend** is a plugin bundle which builds on and improves +**django CMS Frontend** is a plugin bundle which originally built on and improved the architecture of `djangocms-bootstrap4 `_. -Its objective is to provide a set of popular frontend components independent of the -currently used frontend framework such as Bootstrap, or its specific version. +Its objective is to provide a toolset to quickly create re-usable frontend +components and comes preloaded with a set of popular frontend components +independent of the currently used frontend framework such as Bootstrap, or +its specific version. .. image:: preview.png Key features ============ +- **Easy to implement re-usable frontend custom components**, which in the + simplest case consist of a template and declarative sort of form class. + - Support of `Bootstrap 5 `_, django CMS 3.8+ - and django CMS 4. + and django CMS 4 out of the box. - **Separation of plugins from css framework**, i.e. no need to rebuild you site's plugin tree if css framework is changed in the future, e.g. from Bootstrap 5 to a future version. -- **New link plugin** allowing to link to internal pages provided by - other applications, such as `djangocms-blog +- Leverage of new **djangocms-link features** allowing to link to internal pages + provided by other applications, such as `djangocms-blog `_. -- **Nice and well-arranged admin frontend** of `djangocms-bootstrap4 - `_ +- **Nice and well-arranged admin frontend** of djangocms-bootstrap4 - **Extensible** within the project and with separate project (e.g. a theme app). Create your own components with a few lines of code only. @@ -35,10 +39,6 @@ Key features (e.g. in a custom app) giving your whole project a more consistent user experience. -- A management command to **migrate from djangocms-bootstrap4**. This - command automatically migrates all ``djangocms-bootstrap4`` plugins to - ``djangocms-frontend``. - Description =========== @@ -55,10 +55,6 @@ Instead all design parameters are stored in a common JSON field and future releases of improved frontend features will not require to rebuild your full plugin tree. -The link plugin has been rewritten to not only allow internal links to other -CMS pages, but also to other django models such as, e.g., posts of -`djangocms-blog `_. - The plugins are designed to be re-usable as UI components in your project, e.g. in a custom app, giving your whole project a more consistent user experience. @@ -98,8 +94,7 @@ file for additional dependencies: - django-cms, version 3.7 or later - django-filer, version 1.7 or later - djangocms-attributes-field, version 1.0 or later -- djangocms-text-ckeditor, version 3.1 or later -- django-select2 +- djangocms-text - django-entangled Make sure `django Filer @@ -121,23 +116,22 @@ For a manual install: 'easy_thumbnails', 'djangocms_frontend', - 'djangocms_frontend.contrib.accordion', - 'djangocms_frontend.contrib.alert', - 'djangocms_frontend.contrib.badge', - 'djangocms_frontend.contrib.card', - 'djangocms_frontend.contrib.carousel', - 'djangocms_frontend.contrib.collapse', - 'djangocms_frontend.contrib.component', - 'djangocms_frontend.contrib.content', - 'djangocms_frontend.contrib.grid', - 'djangocms_frontend.contrib.icon', - 'djangocms_frontend.contrib.image', - 'djangocms_frontend.contrib.jumbotron', - 'djangocms_frontend.contrib.link', - 'djangocms_frontend.contrib.listgroup', - 'djangocms_frontend.contrib.media', - 'djangocms_frontend.contrib.tabs', - 'djangocms_frontend.contrib.utilities', + 'djangocms_frontend.contrib.accordion', # optional + 'djangocms_frontend.contrib.alert', # optional + 'djangocms_frontend.contrib.badge', # optional + 'djangocms_frontend.contrib.card', # optional + 'djangocms_frontend.contrib.carousel', # optional + 'djangocms_frontend.contrib.collapse', # optional + 'djangocms_frontend.contrib.content', # optional + 'djangocms_frontend.contrib.grid', # optional + 'djangocms_frontend.contrib.icon', # optional + 'djangocms_frontend.contrib.image', # optional + 'djangocms_frontend.contrib.jumbotron', # optional + 'djangocms_frontend.contrib.link', # optional + 'djangocms_frontend.contrib.listgroup', # optional + 'djangocms_frontend.contrib.media', # optional + 'djangocms_frontend.contrib.tabs', # optional + 'djangocms_frontend.contrib.utilities', # optional - run ``python manage.py migrate`` @@ -153,8 +147,7 @@ install separately or by adding an option: Documentation ============= -See readthedocs for the `documentation -`_. +See readthedocs for the `documentation `_. License ======= diff --git a/run_tests.py b/conftest.py similarity index 81% rename from run_tests.py rename to conftest.py index cbc597a3..c33bbf4a 100755 --- a/run_tests.py +++ b/conftest.py @@ -7,17 +7,17 @@ from django.test.utils import get_runner -def run(argv=None): - if argv is None: - argv = ["tests"] - tests = argv[1:] if len(argv) > 1 else ["tests"] +def pytest_configure(): os.environ["DJANGO_SETTINGS_MODULE"] = "tests.test_settings" django.setup() + + +if __name__ == "__main__": + pytest_configure() + + argv = ["tests"] if sys.argv is None else sys.argv + tests = argv[1:] if len(argv) > 1 else ["tests"] TestRunner = get_runner(settings) test_runner = TestRunner() failures = test_runner.run_tests(tests) sys.exit(bool(failures)) - - -if __name__ == "__main__": - run(sys.argv) diff --git a/djangocms_frontend/__init__.py b/djangocms_frontend/__init__.py index f4bc00fb..0a6d3925 100644 --- a/djangocms_frontend/__init__.py +++ b/djangocms_frontend/__init__.py @@ -19,4 +19,4 @@ 13. Github actions will publish the new package to pypi """ -__version__ = "2.0.0a" +__version__ = "2.0.0a1" diff --git a/djangocms_frontend/apps.py b/djangocms_frontend/apps.py index 444c1cee..919e378e 100644 --- a/djangocms_frontend/apps.py +++ b/djangocms_frontend/apps.py @@ -1,11 +1,68 @@ from django import apps +from django.core import checks class DjangocmsFrontendConfig(apps.AppConfig): name = "djangocms_frontend" - verbose_name = "DjangoCMS Frontend" + verbose_name = "django CMS Frontend" def ready(self): - from .component_pool import setup + from . import plugin_tag - setup() + plugin_tag.setup() + checks.register(check_settings) + checks.register(check_installed_apps) + + +def check_settings(*args, **kwargs): # pragma: no cover + from django.conf import settings + + warnings = [] + + if hasattr(settings, "DJANGOCMS_FRONTEND_MINIMUM_INPUT_LENGTH"): + warnings.append( + checks.Warning( + "The DJANGOCMS_FRONTEND_MINIMUM_INPUT_LENGTH setting was removed in djangocms-frontend 2.", + "Use DJANGOCMS_LINK_MINIMUM_INPUT_LENGTH instead.", + id="djangocms_frontend.W001", + obj="settings.DJANGOCMS_FRONTEND_MINIMUM_INPUT_LENGTH", + ) + ) + if hasattr(settings, "DJANGOCMS_FRONTEND_LINK_MODELS"): + warnings.append( + checks.Warning( + "The DJANGOCMS_FRONTEND_LINK_MODELS setting was removed in djangocms-frontend 2. " + "djangocms-frontend 2 uses linkable models from djangocms-link. See " + "https://github.com/django-cms/djangocms-link#django-cms-link for more info.", + "This message disappears after removing the DJANGOCMS_FRONTEND_LINK_MODELS from your " + "project's settings.\n", + id="djangocms_frontend.W002", + obj="settings.DJANGOCMS_FRONTEND_LINK_MODELS", + ) + ) + return warnings + + +def check_installed_apps(*args, **kwargs): # pragma: no cover + from django.conf import settings + + errors = [] + link_contrib_apps = [ + "djangocms_frontend.contrib.carousel", + "djangocms_frontend.contrib.image", + "djangocms_frontend.contrib.link", + ] + link_apps_used = [app for app in link_contrib_apps if app in settings.INSTALLED_APPS] + if link_apps_used: + if "djangocms_link" not in settings.INSTALLED_APPS: + errors.append( + checks.Error( + "djangocms-frontend requires djangocms-link to be installed for {}.".format( + ", ".join(link_apps_used) + ), + "Add 'djangocms_link' to your INSTALLED_APPS setting or remove all of the above apps.", + id="djangocms_frontend.E001", + obj="settings.INSTALLED_APPS", + ) + ) + return errors diff --git a/djangocms_frontend/cms_plugins.py b/djangocms_frontend/cms_plugins.py index d4610163..d36657c7 100644 --- a/djangocms_frontend/cms_plugins.py +++ b/djangocms_frontend/cms_plugins.py @@ -1,44 +1,23 @@ -from cms.constants import SLUG_REGEXP -from cms.plugin_base import CMSPluginBase -from django.utils.encoding import force_str +from cms.plugin_pool import plugin_pool -from djangocms_frontend.helpers import get_related +from .component_pool import components +from .ui_plugin_base import CMSUIPluginBase -if hasattr(CMSPluginBase, "edit_field"): - # FrontendEditable functionality already implemented in core? - FrontendEditableAdminMixin = object -else: - # If not use our own version of the plugin-enabled mixin - from .helpers import FrontendEditableAdminMixin +class CMSUIPlugin(CMSUIPluginBase): + pass -class CMSUIPlugin(FrontendEditableAdminMixin, CMSPluginBase): - render_template = "djangocms_frontend/html_container.html" - change_form_template = "djangocms_frontend/admin/base.html" - def __str__(self): - return force_str(super().__str__()) +# Loop through the values in the components' registry +for _, plugin, slot_plugins in components._registry.values(): + # Add the plugin to the global namespace + globals()[plugin.__name__] = plugin + # Register the plugin with the plugin pool + plugin_pool.register_plugin(plugin) - def render(self, context, instance, placeholder): - for key, value in instance.config.items(): - if isinstance(value, dict) and set(value.keys()) == {"pk", "model"}: - if key not in instance.__dir__(): # hasattr would return the value in the config dict - setattr(instance.__class__, key, get_related(key)) - return super().render(context, instance, placeholder) - - def get_plugin_urls(self): - from django.urls import re_path - - info = f"{self.model._meta.app_label}_{self.model._meta.model_name}" - - def pat(regex, fn): - return re_path(regex, fn, name=f"{info}_{fn.__name__}") - - return [ - pat(r'edit-field/(%s)/([a-z\-]+)/$' % SLUG_REGEXP, self.edit_field), - ] - - def _get_object_for_single_field(self, object_id, language): - from .models import FrontendUIItem - - return FrontendUIItem.objects.get(pk=object_id) + # Loop through the slot plugins associated with the current plugin + for slot_plugin in slot_plugins: + # Add the slot plugin to the global namespace + globals()[slot_plugin.__name__] = slot_plugin + # Register the slot plugin with the plugin pool + plugin_pool.register_plugin(slot_plugin) diff --git a/djangocms_frontend/common/__init__.py b/djangocms_frontend/common/__init__.py index 0957b32d..ccec0e60 100644 --- a/djangocms_frontend/common/__init__.py +++ b/djangocms_frontend/common/__init__.py @@ -2,14 +2,21 @@ from djangocms_frontend import settings +from .attributes import AttributesMixin, AttributesFormMixin from .title import TitleFormMixin, TitleMixin __common = { - "attributes": ("AttributesMixin",), "background": ("BackgroundFormMixin", "BackgroundMixin"), "responsive": ("ResponsiveFormMixin", "ResponsiveMixin"), "sizing": ("SizingFormMixin", "SizingMixin"), - "spacing": ("SpacingFormMixin", "SpacingMixin", "MarginFormMixin", "MarginMixin", "PaddingFormMixin", "PaddingMixin"), + "spacing": ( + "SpacingFormMixin", + "SpacingMixin", + "MarginFormMixin", + "MarginMixin", + "PaddingFormMixin", + "PaddingMixin", + ), } for module, classes in __common.items(): @@ -25,6 +32,7 @@ "TitleMixin", "TitleFormMixin", "AttributesMixin", + "AttributesFormMixin", "BackgroundFormMixin", "BackgroundMixin", "ResponsiveFormMixin", diff --git a/djangocms_frontend/common/bootstrap5/attributes.py b/djangocms_frontend/common/attributes.py similarity index 71% rename from djangocms_frontend/common/bootstrap5/attributes.py rename to djangocms_frontend/common/attributes.py index 4bc0d617..f0fb6057 100644 --- a/djangocms_frontend/common/bootstrap5/attributes.py +++ b/djangocms_frontend/common/attributes.py @@ -1,5 +1,7 @@ from django.utils.translation import gettext_lazy as _ +from entangled.forms import EntangledModelFormMixin +from djangocms_frontend.fields import AttributesFormField from djangocms_frontend.helpers import insert_fields @@ -25,3 +27,13 @@ def get_fieldsets(self, request, obj=None): blockattrs=self.block_attr, position=-1, # Always last ) + + +class AttributesFormMixin(EntangledModelFormMixin): + class Meta: + entangled_fields = { + "config": [ + "attributes", + ] + } + attributes = AttributesFormField() diff --git a/djangocms_frontend/common/bootstrap5/background.py b/djangocms_frontend/common/bootstrap5/background.py index d930c808..235aff22 100644 --- a/djangocms_frontend/common/bootstrap5/background.py +++ b/djangocms_frontend/common/bootstrap5/background.py @@ -4,7 +4,7 @@ from djangocms_frontend import settings from djangocms_frontend.fields import ButtonGroup, ColoredButtonGroup -from djangocms_frontend.helpers import first_choice, insert_fields +from djangocms_frontend.helpers import insert_fields class BackgroundMixin: @@ -23,9 +23,8 @@ def get_fieldsets(self, request, obj=None): def render(self, context, instance, placeholder): if getattr(instance, "background_context", ""): instance.add_classes(f"bg-{instance.background_context}") - if getattr(instance, "background_opacity", "100") != "100": - if instance.background_opacity: - instance.add_classes(f"bg-opacity-{instance.background_opacity}") + if getattr(instance, "background_opacity", ""): + instance.add_classes(f"bg-opacity-{instance.background_opacity}") if getattr(instance, "background_shadow", ""): if instance.background_shadow == "reg": instance.add_classes("shadow") @@ -54,8 +53,8 @@ class Meta: background_opacity = forms.ChoiceField( label=_("Background opacity"), required=False, - choices=settings.framework_settings.OPACITY_CHOICES, - initial=first_choice(settings.framework_settings.OPACITY_CHOICES), + choices=settings.EMPTY_CHOICE + settings.framework_settings.OPACITY_CHOICES, + initial=settings.EMPTY_CHOICE[0][0], widget=ButtonGroup(attrs=dict(property="opacity")), help_text=_("Opacity of card background color (only if no outline selected)"), ) diff --git a/djangocms_frontend/common/bootstrap5/responsive.py b/djangocms_frontend/common/bootstrap5/responsive.py index e9a0a04f..2fcdb8e6 100644 --- a/djangocms_frontend/common/bootstrap5/responsive.py +++ b/djangocms_frontend/common/bootstrap5/responsive.py @@ -36,7 +36,7 @@ def get_fieldsets(self, request, obj=None): ) def render(self, context, instance, placeholder): - if instance.config.get("responsive_visibility", None) is not None: + if instance.config.get("responsive_visibility", None): instance.add_classes( get_display_classes( instance.responsive_visibility, diff --git a/djangocms_frontend/common/title.py b/djangocms_frontend/common/title.py index e63c88db..f9983231 100644 --- a/djangocms_frontend/common/title.py +++ b/djangocms_frontend/common/title.py @@ -61,6 +61,7 @@ class Meta: plugin_title = TitleField( label=_("Title"), required=False, + initial={"show": False, "title": ""}, help_text=_( "Optional title of the plugin for easier identification. " "Its title attribute " diff --git a/djangocms_frontend/contrib/component/components.py b/djangocms_frontend/component_base.py similarity index 88% rename from djangocms_frontend/contrib/component/components.py rename to djangocms_frontend/component_base.py index f8d4d603..eaa74cec 100644 --- a/djangocms_frontend/contrib/component/components.py +++ b/djangocms_frontend/component_base.py @@ -9,6 +9,15 @@ from django.utils.translation import gettext_lazy as _ from entangled.forms import EntangledModelForm +from .ui_plugin_base import CMSUIPluginBase + + +def _import_or_empty(module, name): + try: + return importlib.import_module(module).__dict__[name] + except (ImportError, KeyError): + return type(name, (), {}) + def _get_mixin_classes(mixins: list, suffix: str = "") -> list[type]: """Find and import mixin classes from a list of mixin strings""" @@ -18,7 +27,8 @@ def _get_mixin_classes(mixins: list, suffix: str = "") -> list[type]: else ("djangocms_frontend.common", f"{mixin}{suffix}Mixin") for mixin in mixins ] - return [importlib.import_module(module).__dict__[name] for module, name in mixins] + + return [_import_or_empty(module, name) for module, name in mixins] class Slot: @@ -112,8 +122,6 @@ def plugin_model_factory(cls) -> type: @classmethod def plugin_factory(cls) -> type: if cls._plugin is None: - from djangocms_frontend.cms_plugins import CMSUIPlugin - mixins = getattr(cls._component_meta, "mixins", []) slots = cls.get_slot_plugins() mixins = _get_mixin_classes(mixins) @@ -123,16 +131,18 @@ def plugin_factory(cls) -> type: ( *mixins, *cls._plugin_mixins, - CMSUIPlugin, + CMSUIPluginBase, ), { "name": getattr(cls._component_meta, "name", cls.__name__), - "module": getattr(cls._component_meta, "module", _("Component")), + "module": getattr(cls._component_meta, "module", _("Components")), "model": cls.plugin_model_factory(), "form": cls.admin_form_factory(), "allow_children": getattr(cls._component_meta, "allow_children", False) or slots, + "require_parent": getattr(cls._component_meta, "require_parent", False), "child_classes": getattr(cls._component_meta, "child_classes", []) + list(slots.keys()), - "render_template": getattr(cls._component_meta, "render_template", CMSUIPlugin.render_template), + "parent_classes": getattr(cls._component_meta, "parent_classes", []), + "render_template": getattr(cls._component_meta, "render_template", CMSUIPluginBase.render_template), "fieldsets": getattr(cls, "fieldsets", cls._generate_fieldset()), "change_form_template": "djangocms_frontend/admin/base.html", "slots": slots, @@ -182,9 +192,7 @@ def get_registration(cls) -> tuple[type, type, list[type]]: @classmethod @property def _component_meta(cls) -> typing.Optional[type]: - if hasattr(cls, "Meta"): - return cls.Meta - return None + return getattr(cls, "Meta", None) @classmethod def _generate_fieldset(cls) -> list[tuple[typing.Optional[str], dict]]: @@ -194,20 +202,9 @@ def get_short_description(self) -> str: return self.config.get("title", "") def save_model(self, request, obj, form: forms.Form, change: bool) -> None: - """Auto-createas slot plugins upon creation of component plugin instance""" - from djangocms_frontend.cms_plugins import CMSUIPlugin + """Auto-creates slot plugins upon creation of component plugin instance""" - super(CMSUIPlugin, self).save_model(request, obj, form, change) + super(CMSUIPluginBase, self).save_model(request, obj, form, change) if not change: for slot in self.slots.keys(): add_plugin(obj.placeholder, slot, obj.language, target=obj) - - -class ComponentLinkMixin: - from djangocms_frontend.contrib.link.cms_plugins import LinkPluginMixin - from djangocms_frontend.contrib.link.forms import AbstractLinkForm - from djangocms_frontend.contrib.link.helpers import GetLinkMixin - - _base_form = AbstractLinkForm - _model_mixins = [GetLinkMixin] - _plugin_mixins = [LinkPluginMixin] diff --git a/djangocms_frontend/component_pool.py b/djangocms_frontend/component_pool.py index c1024d32..95c8fb8d 100644 --- a/djangocms_frontend/component_pool.py +++ b/djangocms_frontend/component_pool.py @@ -1,97 +1,25 @@ -import copy -import importlib import warnings -from cms.plugin_pool import plugin_pool -from cms.templatetags.cms_tags import render_plugin -from django.conf import settings -from django.contrib.admin.sites import site as admin_site -from django.template import engines -from django.template.library import SimpleNode -from django.template.loader import get_template +from django.utils.module_loading import autodiscover_modules -django_engine = engines["django"] -plugin_tag_pool = {} +class Components: + _registry: dict = {} + _discovered: bool = False + def register(self, component): + if component.__name__ in self._registry: + warnings.warn(f"Component {component.__name__} already registered", stacklevel=2) + return component + self._registry[component.__name__] = component.get_registration() + return component -IGNORED_FIELDS = ( - "id", - "cmsplugin_ptr", - "language", - "plugin_type", - "position", - "creation_date", - "ui_item", -) + def __getitem__(self, item): + return self._registry[item] -allowed_plugin_types = tuple( - getattr(importlib.import_module(cls.rsplit(".", 1)[0]), cls.rsplit(".", 1)[-1]) if isinstance(cls, str) else cls - for cls in getattr(settings, "CMS_COMPONENT_PLUGINS", []) -) +components = Components() -def _get_plugindefaults(instance): - defaults = { - field.name: getattr(instance, field.name) - for field in instance._meta.fields - if field.name not in IGNORED_FIELDS and bool(getattr(instance, field.name)) - } - defaults["plugin_type"] = instance.__class__.__name__ - return defaults - - -class _DummyUser: - is_superuser = True - is_staff = True - - -class _DummyRequest: - user = _DummyUser() - - -def render_dummy_plugin(context, dummy_plugin): - return dummy_plugin.nodelist.render(context) - - -def patch_template(template): - """Patches the template to use the dummy plugin renderer instead of the real one.""" - copied_template = copy.deepcopy(template) - patch = False - for node in copied_template.template.nodelist.get_nodes_by_type(SimpleNode): - if node.func == render_plugin: - patch = True - node.func = render_dummy_plugin - return copied_template if patch else template - - -def setup(): - global plugin_tag_pool - - for plugin in plugin_pool.get_all_plugins(): - if not issubclass(plugin, allowed_plugin_types): - continue - tag_name = plugin.__name__.lower() - if tag_name.endswith("plugin"): - tag_name = tag_name[:-6] - try: - instance = plugin.model() # Create instance with defaults - plugin_admin = plugin(admin_site=admin_site) - if hasattr(instance, "initialize_from_form"): - instance.initialize_from_form(plugin.form) - if tag_name not in plugin_tag_pool: - template = get_template(plugin_admin._get_render_template({"request": None}, instance, None)) - plugin_tag_pool[tag_name] = { - "defaults": { - **_get_plugindefaults(instance), - **dict(plugin_type=plugin.__name__), - }, - "template": patch_template(template), - "class": plugin, - } - else: # pragma: no cover - warnings.warn( - f"Duplicate candidates for {{% plugin \"{tag_name}\" %}} found. " - f"Only registered {plugin_tag_pool[tag_name]['class'].__name__}.", stacklevel=1) - except Exception as exc: # pragma: no cover - warnings.warn(f"{plugin.__name__}: \n{str(exc)}", stacklevel=1) +if not components._discovered: + autodiscover_modules("cms_components", register_to=components) + components._discovered = True diff --git a/djangocms_frontend/contrib/alert/forms.py b/djangocms_frontend/contrib/alert/forms.py index 65ad4698..4885f2de 100644 --- a/djangocms_frontend/contrib/alert/forms.py +++ b/djangocms_frontend/contrib/alert/forms.py @@ -5,11 +5,7 @@ from djangocms_frontend import settings from djangocms_frontend.common import ResponsiveFormMixin, SpacingFormMixin from djangocms_frontend.contrib import alert -from djangocms_frontend.fields import ( - AttributesFormField, - ColoredButtonGroup, - TagTypeFormField, -) +from djangocms_frontend.fields import AttributesFormField, ColoredButtonGroup, TagTypeFormField from djangocms_frontend.helpers import first_choice from djangocms_frontend.models import FrontendUIItem from djangocms_frontend.settings import COLOR_STYLE_CHOICES diff --git a/djangocms_frontend/contrib/badge/forms.py b/djangocms_frontend/contrib/badge/forms.py index f71a6b45..37fa55e6 100644 --- a/djangocms_frontend/contrib/badge/forms.py +++ b/djangocms_frontend/contrib/badge/forms.py @@ -2,11 +2,7 @@ from django.utils.translation import gettext_lazy as _ from entangled.forms import EntangledModelForm -from djangocms_frontend.fields import ( - AttributesFormField, - ColoredButtonGroup, - TagTypeFormField, -) +from djangocms_frontend.fields import AttributesFormField, ColoredButtonGroup, TagTypeFormField from djangocms_frontend.helpers import first_choice from djangocms_frontend.models import FrontendUIItem from djangocms_frontend.settings import COLOR_STYLE_CHOICES diff --git a/djangocms_frontend/contrib/card/cms_plugins.py b/djangocms_frontend/contrib/card/cms_plugins.py index e686efe7..38d7354a 100644 --- a/djangocms_frontend/contrib/card/cms_plugins.py +++ b/djangocms_frontend/contrib/card/cms_plugins.py @@ -3,13 +3,7 @@ from ... import settings from ...cms_plugins import CMSUIPlugin -from ...common import ( - AttributesMixin, - BackgroundMixin, - MarginMixin, - PaddingMixin, - ResponsiveMixin, -) +from ...common import AttributesMixin, BackgroundMixin, MarginMixin, PaddingMixin, ResponsiveMixin from ...helpers import add_plugin from .. import card from . import forms, models diff --git a/djangocms_frontend/contrib/card/forms.py b/djangocms_frontend/contrib/card/forms.py index d56c6a98..0bceb364 100644 --- a/djangocms_frontend/contrib/card/forms.py +++ b/djangocms_frontend/contrib/card/forms.py @@ -7,28 +7,13 @@ from djangocms_frontend.settings import COLOR_STYLE_CHOICES, DEVICE_SIZES from ... import settings -from ...common import ( - BackgroundFormMixin, - MarginFormMixin, - PaddingFormMixin, - ResponsiveFormMixin, -) -from ...fields import ( - AttributesFormField, - ButtonGroup, - ColoredButtonGroup, - IconGroup, - TagTypeFormField, -) +from ...common import BackgroundFormMixin, MarginFormMixin, PaddingFormMixin, ResponsiveFormMixin +from ...fields import AttributesFormField, ButtonGroup, ColoredButtonGroup, IconGroup, TagTypeFormField from ...helpers import first_choice, link_to_framework_doc from ...models import FrontendUIItem from .. import card from ..grid.constants import GRID_SIZE -from .constants import ( - CARD_ALIGNMENT_CHOICES, - CARD_INNER_TYPE_CHOICES, - CARD_LAYOUT_TYPE_CHOICES, -) +from .constants import CARD_ALIGNMENT_CHOICES, CARD_INNER_TYPE_CHOICES, CARD_LAYOUT_TYPE_CHOICES # card allow for a transparent color CARD_COLOR_STYLE_CHOICES = settings.COLOR_STYLE_CHOICES + (("transparent", _("Transparent")),) diff --git a/djangocms_frontend/contrib/card/frameworks/bootstrap5.py b/djangocms_frontend/contrib/card/frameworks/bootstrap5.py index 1e7499e0..2674e5a7 100644 --- a/djangocms_frontend/contrib/card/frameworks/bootstrap5.py +++ b/djangocms_frontend/contrib/card/frameworks/bootstrap5.py @@ -1,6 +1,4 @@ -from djangocms_frontend.contrib.grid.frameworks.bootstrap5 import ( - get_row_cols_grid_values, -) +from djangocms_frontend.contrib.grid.frameworks.bootstrap5 import get_row_cols_grid_values class CardRenderMixin: diff --git a/djangocms_frontend/contrib/carousel/cms_plugins.py b/djangocms_frontend/contrib/carousel/cms_plugins.py index 421db65d..f004234d 100644 --- a/djangocms_frontend/contrib/carousel/cms_plugins.py +++ b/djangocms_frontend/contrib/carousel/cms_plugins.py @@ -81,18 +81,8 @@ class CarouselSlidePlugin( ) }, ), - ( - _("Link settings"), - { - "classes": ("collapse",), - "fields": ( - ("external_link", "internal_link"), - ("mailto", "phone"), - ("anchor", "target"), - ), - }, - ), ] + link_fieldset_position = 1 def get_render_template(self, context, instance, placeholder): return get_plugin_template( diff --git a/djangocms_frontend/contrib/carousel/forms.py b/djangocms_frontend/contrib/carousel/forms.py index ec2a09e3..ac599722 100644 --- a/djangocms_frontend/contrib/carousel/forms.py +++ b/djangocms_frontend/contrib/carousel/forms.py @@ -5,12 +5,7 @@ from filer.fields.image import AdminImageFormField, FilerImageField from filer.models import Image -from djangocms_frontend.fields import ( - AttributesFormField, - ButtonGroup, - TagTypeFormField, - TemplateChoiceMixin, -) +from djangocms_frontend.fields import AttributesFormField, ButtonGroup, TagTypeFormField, TemplateChoiceMixin from ... import settings from ...common import BackgroundFormMixin @@ -18,7 +13,7 @@ from ...helpers import first_choice from ...models import FrontendUIItem from .. import carousel -from ..link.forms import AbstractLinkForm +from ..link.forms import LinkFormMixin from .constants import ( CAROUSEL_ASPECT_RATIO_CHOICES, CAROUSEL_PAUSE_CHOICES, @@ -143,8 +138,8 @@ class Meta: class CarouselSlideForm( mixin_factory("CarouselSlide"), - AbstractLinkForm, BackgroundFormMixin, + LinkFormMixin, EntangledModelForm, ): """ diff --git a/djangocms_frontend/contrib/component/__init__.py b/djangocms_frontend/contrib/component/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/djangocms_frontend/contrib/component/cms_plugins.py b/djangocms_frontend/contrib/component/cms_plugins.py deleted file mode 100644 index 896bcab0..00000000 --- a/djangocms_frontend/contrib/component/cms_plugins.py +++ /dev/null @@ -1,18 +0,0 @@ -from cms.plugin_pool import plugin_pool - -# Import the components from the current directory's models module -from .registry import components - -# Loop through the values in the components' registry -for _, plugin, slot_plugins in components._registry.values(): - # Add the plugin to the global namespace - globals()[plugin.__name__] = plugin - # Register the plugin with the plugin pool - plugin_pool.register_plugin(plugin) - - # Loop through the slot plugins associated with the current plugin - for slot_plugin in slot_plugins: - # Add the slot plugin to the global namespace - globals()[slot_plugin.__name__] = slot_plugin - # Register the slot plugin with the plugin pool - plugin_pool.register_plugin(slot_plugin) diff --git a/djangocms_frontend/contrib/component/registry.py b/djangocms_frontend/contrib/component/registry.py deleted file mode 100644 index 95c8fb8d..00000000 --- a/djangocms_frontend/contrib/component/registry.py +++ /dev/null @@ -1,25 +0,0 @@ -import warnings - -from django.utils.module_loading import autodiscover_modules - - -class Components: - _registry: dict = {} - _discovered: bool = False - - def register(self, component): - if component.__name__ in self._registry: - warnings.warn(f"Component {component.__name__} already registered", stacklevel=2) - return component - self._registry[component.__name__] = component.get_registration() - return component - - def __getitem__(self, item): - return self._registry[item] - - -components = Components() - -if not components._discovered: - autodiscover_modules("cms_components", register_to=components) - components._discovered = True diff --git a/djangocms_frontend/contrib/grid/cms_plugins.py b/djangocms_frontend/contrib/grid/cms_plugins.py index 26c01f8a..32b9963d 100644 --- a/djangocms_frontend/contrib/grid/cms_plugins.py +++ b/djangocms_frontend/contrib/grid/cms_plugins.py @@ -2,13 +2,7 @@ from django.utils.translation import gettext_lazy as _ from djangocms_frontend import settings -from djangocms_frontend.common import ( - AttributesMixin, - BackgroundMixin, - ResponsiveMixin, - SizingMixin, - SpacingMixin, -) +from djangocms_frontend.common import AttributesMixin, BackgroundMixin, ResponsiveMixin, SizingMixin, SpacingMixin from ...cms_plugins import CMSUIPlugin from ...common import TitleMixin diff --git a/djangocms_frontend/contrib/grid/forms.py b/djangocms_frontend/contrib/grid/forms.py index 5772fbdd..5af376b1 100644 --- a/djangocms_frontend/contrib/grid/forms.py +++ b/djangocms_frontend/contrib/grid/forms.py @@ -6,19 +6,8 @@ from entangled.forms import EntangledModelForm from djangocms_frontend import settings -from djangocms_frontend.common import ( - BackgroundFormMixin, - ResponsiveFormMixin, - SizingFormMixin, - SpacingFormMixin, -) -from djangocms_frontend.fields import ( - AttributesFormField, - AutoNumberInput, - ButtonGroup, - IconGroup, - TagTypeFormField, -) +from djangocms_frontend.common import BackgroundFormMixin, ResponsiveFormMixin, SizingFormMixin, SpacingFormMixin +from djangocms_frontend.fields import AttributesFormField, AutoNumberInput, ButtonGroup, IconGroup, TagTypeFormField from djangocms_frontend.helpers import first_choice, link_to_framework_doc from djangocms_frontend.models import FrontendUIItem diff --git a/djangocms_frontend/contrib/icon/forms.py b/djangocms_frontend/contrib/icon/forms.py index 526b3f6c..7e09ef81 100644 --- a/djangocms_frontend/contrib/icon/forms.py +++ b/djangocms_frontend/contrib/icon/forms.py @@ -2,11 +2,7 @@ from django.utils.translation import gettext_lazy as _ from entangled.forms import EntangledModelForm -from djangocms_frontend.fields import ( - AttributesFormField, - ColoredButtonGroup, - TagTypeFormField, -) +from djangocms_frontend.fields import AttributesFormField, ColoredButtonGroup, TagTypeFormField from ... import settings from ...common import BackgroundFormMixin, ResponsiveFormMixin, SpacingFormMixin diff --git a/djangocms_frontend/contrib/image/forms.py b/djangocms_frontend/contrib/image/forms.py index 0a1043e1..09786199 100644 --- a/djangocms_frontend/contrib/image/forms.py +++ b/djangocms_frontend/contrib/image/forms.py @@ -2,7 +2,6 @@ from django.conf import settings as django_settings from django.db.models.fields.related import ManyToOneRel from django.utils.translation import gettext_lazy as _ -from entangled.forms import EntangledModelForm from filer.fields.image import AdminImageFormField, FilerImageField from filer.models import Image, ThumbnailOption @@ -45,13 +44,6 @@ def get_templates(): PICTURE_ALIGNMENT = get_alignment() -LINK_TARGET = ( - ("_blank", _("Open in new window")), - ("_self", _("Open in same window")), - ("_parent", _("Delegate to parent")), - ("_top", _("Delegate to top")), -) - RESPONSIVE_IMAGE_CHOICES = ( ("inherit", _("Let settings.DJANGOCMS_PICTURE_RESPONSIVE_IMAGES decide")), ("yes", _("Yes")), @@ -61,10 +53,9 @@ def get_templates(): class ImageForm( TemplateChoiceMixin, - AbstractLinkForm, ResponsiveFormMixin, MarginFormMixin, - EntangledModelForm, + AbstractLinkForm, ): """ Content > "Image" Plugin @@ -217,24 +208,6 @@ class Meta: def clean(self): super().clean() data = self.cleaned_data - # there can be only one link type - if ( - sum( - ( - bool(data.get("external_link", False)), - bool(data.get("internal_link", False)), - bool(data.get("file_link", False)), - ) - ) - > 1 - ): - raise forms.ValidationError( - _( - "You have given more than one external, internal, or file link target. " - "Only one option is allowed." - ) - ) - # you shall only set one image kind if not data.get("picture", False) and not data.get("external_picture", False): raise forms.ValidationError( diff --git a/djangocms_frontend/contrib/jumbotron/forms.py b/djangocms_frontend/contrib/jumbotron/forms.py index 0eb2943a..bba2c1a1 100644 --- a/djangocms_frontend/contrib/jumbotron/forms.py +++ b/djangocms_frontend/contrib/jumbotron/forms.py @@ -3,18 +3,10 @@ from entangled.forms import EntangledModelForm from djangocms_frontend import settings -from djangocms_frontend.common import ( - BackgroundFormMixin, - ResponsiveFormMixin, - SpacingFormMixin, -) +from djangocms_frontend.common import BackgroundFormMixin, ResponsiveFormMixin, SpacingFormMixin from djangocms_frontend.contrib import jumbotron from djangocms_frontend.contrib.jumbotron import models -from djangocms_frontend.fields import ( - AttributesFormField, - TagTypeFormField, - TemplateChoiceMixin, -) +from djangocms_frontend.fields import AttributesFormField, TagTypeFormField, TemplateChoiceMixin from djangocms_frontend.helpers import first_choice mixin_factory = settings.get_forms(jumbotron) diff --git a/djangocms_frontend/contrib/link/cms_plugins.py b/djangocms_frontend/contrib/link/cms_plugins.py index dd6c0564..31720643 100644 --- a/djangocms_frontend/contrib/link/cms_plugins.py +++ b/djangocms_frontend/contrib/link/cms_plugins.py @@ -1,7 +1,6 @@ from cms.plugin_pool import plugin_pool from django.apps import apps from django.conf import settings as django_settings -from django.urls import path from django.utils.translation import gettext_lazy as _ from djangocms_frontend.helpers import get_plugin_template, insert_fields @@ -10,15 +9,16 @@ from ...cms_plugins import CMSUIPlugin from ...common import AttributesMixin, SpacingMixin from .. import link -from . import forms, models, views +from . import forms, models from .constants import USE_LINK_ICONS +from .helpers import GetLinkMixin mixin_factory = settings.get_renderer(link) UILINK_FIELDS = ( ("name", "link_type"), - ("site", "url_grouper") if apps.is_installed("djangocms_url_manager") else ("external_link", "internal_link"), + ("site", "url_grouper") if apps.is_installed("djangocms_url_manager") else "link", ("link_context", "link_size"), ("link_outline", "link_block"), "link_stretched", @@ -33,20 +33,6 @@ }, ), ] -if not apps.is_installed("djangocms_url_manager"): - UILINK_FIELDSET += [ - ( - _("Link settings"), - { - "classes": ("collapse",), - "fields": ( - ("mailto", "phone"), - ("anchor", "target"), - ("file_link",), - ), - }, - ), - ] class LinkPluginMixin: @@ -54,17 +40,13 @@ class LinkPluginMixin: link_fields = ( (("site", "url_grouper"),) if apps.is_installed("djangocms_url_manager") - else ( - ("external_link", "internal_link"), - ("mailto", "phone"), - ("anchor", "target"), - "file_link", - ) + else ("link", "target") ) def render(self, context, instance, placeholder): if "request" in context: instance._cms_page = getattr(context["request"], "current_page", None) + context["link"] = instance.get_link() return super().render(context, instance, placeholder) def get_form(self, request, obj=None, change=False, **kwargs): @@ -85,7 +67,7 @@ def get_fieldsets(self, request, obj=None): return fieldsets -class TextLinkPlugin(mixin_factory("Link"), AttributesMixin, SpacingMixin, LinkPluginMixin, CMSUIPlugin): +class TextLinkPlugin(mixin_factory("Link"), AttributesMixin, SpacingMixin, LinkPluginMixin, GetLinkMixin, CMSUIPlugin): """ Components > "Button" Plugin https://getbootstrap.com/docs/5.0/components/buttons/ @@ -111,12 +93,12 @@ class TextLinkPlugin(mixin_factory("Link"), AttributesMixin, SpacingMixin, LinkP def get_render_template(self, context, instance, placeholder): return get_plugin_template(instance, "link", "link", settings.LINK_TEMPLATE_CHOICES) - def get_plugin_urls(self): - return [ - path("autocomplete/", views.AutocompleteJsonView.as_view(), name="link_link_autocomplete"), - ] - if "djangocms_frontend.contrib.link" in django_settings.INSTALLED_APPS: # Only register plugin if in INSTALLED_APPS plugin_pool.register_plugin(TextLinkPlugin) + + if "djangocms_link" in django_settings.INSTALLED_APPS: + from djangocms_link.cms_plugins import LinkPlugin + + LinkPlugin.parent_classes = [None] # Remove it from the list of valid plugins diff --git a/djangocms_frontend/contrib/link/forms.py b/djangocms_frontend/contrib/link/forms.py index f7e36efd..7918a0c5 100644 --- a/djangocms_frontend/contrib/link/forms.py +++ b/djangocms_frontend/contrib/link/forms.py @@ -1,38 +1,19 @@ -import json -from types import SimpleNamespace - -from cms.utils.urlutils import admin_reverse from django import apps, forms from django.conf import settings as django_settings -from django.contrib.admin.widgets import SELECT2_TRANSLATIONS, AutocompleteMixin -from django.contrib.contenttypes.models import ContentType from django.contrib.sites.models import Site -from django.core.exceptions import ObjectDoesNotExist, ValidationError -from django.db import models -from django.db.models.fields.related import ManyToOneRel -from django.utils.encoding import force_str -from django.utils.translation import get_language from django.utils.translation import gettext as _ +from djangocms_link.fields import LinkFormField # from djangocms_link.validators import IntranetURLValidator -from entangled.forms import EntangledModelForm -from filer.fields.image import AdminFileFormField, FilerFileField -from filer.models import File +from entangled.forms import EntangledModelForm, EntangledModelFormMixin from ... import settings from ...common import SpacingFormMixin -from ...fields import ( - AttributesFormField, - ButtonGroup, - ColoredButtonGroup, - TagTypeFormField, - TemplateChoiceMixin, -) -from ...helpers import first_choice, get_related_object +from ...fields import AttributesFormField, ButtonGroup, ColoredButtonGroup, TagTypeFormField, TemplateChoiceMixin +from ...helpers import first_choice from ...models import FrontendUIItem from .. import link from .constants import LINK_CHOICES, LINK_SIZE_CHOICES, TARGET_CHOICES -from .helpers import get_choices, get_object_for_value mixin_factory = settings.get_forms(link) @@ -52,101 +33,11 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) -HOSTNAME = getattr(settings, "DJANGOCMS_LINK_INTRANET_HOSTNAME_PATTERN", None) -LINK_MODELS = getattr(django_settings, "DJANGOCMS_FRONTEND_LINK_MODELS", []) -MINIMUM_INPUT_LENGTH = getattr(django_settings, "DJANGOCMS_FRONTEND_MINIMUM_INPUT_LENGTH", 0) - - -class Select2jqWidget(AutocompleteMixin, forms.Select): - empty_label = _("Select a destination") - - def __init__(self, *args, **kwargs): - if MINIMUM_INPUT_LENGTH: # no-cover - if "attrs" in kwargs: - kwargs["attrs"].setdefault("data-minimum-input-length", MINIMUM_INPUT_LENGTH) - else: - kwargs["attrs"] = {"data-minimum-input-length": MINIMUM_INPUT_LENGTH} - kwargs.setdefault("admin_site", None) - kwargs.setdefault( - "field", - SimpleNamespace( - name="", model=SimpleNamespace(_meta=SimpleNamespace(app="djangocms_frontend", label="link")) - ), - ) # Fake field properties for autocomplete field (unused by link) - super().__init__(*args, **kwargs) - - def get_url(self): - return admin_reverse("link_link_autocomplete") - - def build_attrs(self, base_attrs, extra_attrs=None): - """ - Set select2's AJAX attributes. - - Attributes can be set using the html5 data attribute. - Nested attributes require a double dash as per - https://select2.org/configuration/data-attributes#nested-subkey-options - """ - attrs = super(forms.Select, self).build_attrs(base_attrs, extra_attrs=extra_attrs) - attrs.setdefault("class", "") - i18n_name = getattr(self, "i18n_name", SELECT2_TRANSLATIONS.get(get_language())) # Django 3.2 compat - attrs.update( - { - "data-ajax--cache": "true", - "data-ajax--delay": 250, - "data-ajax--type": "GET", - "data-ajax--url": self.get_url(), - "data-theme": "admin-autocomplete", - "data-app-label": "app", - "data-model-name": "model", - "data-field-name": "field", - "data-allow-clear": json.dumps(not self.is_required), - "data-placeholder": "", # Allows clearing of the input. - "lang": i18n_name, - "class": attrs["class"] + (" " if attrs["class"] else "") + "admin-autocomplete", - } - ) - return attrs - - def optgroups(self, name, value, attr=None): - groups = super(forms.Select, self).optgroups(name, value) - if not self.is_required and groups: - # Add an empty entry to allow for an empty value to be preselected - groups[0][1].insert(0, self.create_option(name, "", "", False, 0)) - return groups - - -class SmartLinkField(forms.ChoiceField): - widget = Select2jqWidget - - def prepare_value(self, value): - if value: - if isinstance(value, dict): # Entangled dictionary? - try: - app_label, model = value["model"].rsplit(".", 1) - content_type = ContentType.objects.get(app_label=app_label, model=model) - return f"{content_type.id}-{value['pk']}" - except (TypeError, ValueError, KeyError, ObjectDoesNotExist): - pass - elif isinstance(value, models.Model): - content_type = ContentType.objects.get_for_model(value) - return f"{content_type.id}-{value.id}" - return "" - - def clean(self, value): - obj = get_object_for_value(value) - if obj is not None: - return obj - return super().clean(value) - - if apps.apps.is_installed("djangocms_url_manager"): - from djangocms_url_manager.forms import ( - HtmlLinkSiteSelectWidget, - HtmlLinkUrlSelectWidget, - ) + from djangocms_url_manager.forms import HtmlLinkSiteSelectWidget, HtmlLinkUrlSelectWidget from djangocms_url_manager.models import UrlGrouper - class AbstractLinkForm(EntangledModelForm): + class LinkFormMixin(EntangledModelFormMixin): class Meta: entangled_fields = { "config": [ @@ -170,63 +61,22 @@ class Meta: else: - class AbstractLinkForm(EntangledModelForm): + class LinkFormMixin(EntangledModelFormMixin): class Meta: entangled_fields = { "config": [ - "external_link", - "internal_link", - "file_link", - "anchor", - "mailto", - "phone", + "link", "target", ] } link_is_optional = False - # url_validators = [ - # IntranetURLValidator(intranet_host_re=HOSTNAME), - # ] - - external_link = forms.URLField( - label=_("External link"), - required=False, - # validators=url_validators, - help_text=_("Provide a link to an external source."), - ) - internal_link = SmartLinkField( - label=_("Internal link"), - required=False, - help_text=_("If provided, overrides the external link."), - ) - file_link = AdminFileFormField( - rel=ManyToOneRel(FilerFileField, File, "id"), - queryset=File.objects.all(), - to_field_name="id", - label=_("File link"), - required=False, - help_text=_("If provided links a file from the filer app."), - ) - # other link types - anchor = forms.CharField( - label=_("Anchor"), + link = LinkFormField( + label=_("Link"), + initial={}, required=False, - help_text=_( - "Appends the value only after the internal or external link. " - 'Do not include a preceding "#" symbol.' - ), ) - mailto = forms.EmailField( - label=_("Email address"), - required=False, - ) - phone = forms.CharField( - label=_("Phone"), - required=False, - ) - # advanced options target = forms.ChoiceField( label=_("Target"), choices=settings.EMPTY_CHOICE + TARGET_CHOICES, @@ -235,71 +85,11 @@ class Meta: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields["internal_link"].choices = self.get_choices - - def get_choices(self): - if MINIMUM_INPUT_LENGTH == 0: - return get_choices(self.request) - if not self.is_bound: # find initial value - int_link_field = self.fields["internal_link"] - initial = self.get_initial_for_field(int_link_field, "internal_link") - if initial: # Initial set? - obj = get_related_object(dict(obj=initial), "obj") # get it! - if obj is not None: - value = int_link_field.prepare_value(initial) - return ((value, str(obj)),) - return () # nothing found - - def clean(self): - super().clean() - link_field_names = ( - "external_link", - "internal_link", - "mailto", - "phone", - "file_link", - ) - anchor_field_name = "anchor" - field_names_allowed_with_anchor = ( - "external_link", - "internal_link", - ) - anchor_field_verbose_name = force_str(self.fields[anchor_field_name].label) - anchor_field_value = self.cleaned_data.get(anchor_field_name, None) - link_fields = {key: self.cleaned_data.get(key, None) for key in link_field_names} - link_field_verbose_names = {key: force_str(self.fields[key].label) for key in link_fields.keys()} - provided_link_fields = {key: value for key, value in link_fields.items() if value} - - if len(provided_link_fields) > 1: - # Too many fields have a value. - verbose_names = sorted(link_field_verbose_names.values()) - error_msg = _("Only one of {0} or {1} may be given.").format( - ", ".join(verbose_names[:-1]), - verbose_names[-1], - ) - errors = {}.fromkeys(provided_link_fields.keys(), error_msg) - raise ValidationError(errors) + self.fields["link"].required = not self.link_is_optional - if ( - len(provided_link_fields) == 0 - and not self.cleaned_data.get(anchor_field_name, None) - and not self.link_is_optional - ): - raise ValidationError(_("Please provide a link.")) - if anchor_field_value: - for field_name in provided_link_fields.keys(): - if field_name not in field_names_allowed_with_anchor: - error_msg = _("%(anchor_field_verbose_name)s is not allowed together with %(field_name)s") % { - "anchor_field_verbose_name": anchor_field_verbose_name, - "field_name": link_field_verbose_names.get(field_name), - } - raise ValidationError( - { - anchor_field_name: error_msg, - field_name: error_msg, - } - ) +class AbstractLinkForm(LinkFormMixin, EntangledModelForm): + pass class LinkForm(mixin_factory("Link"), SpacingFormMixin, TemplateChoiceMixin, AbstractLinkForm): diff --git a/djangocms_frontend/contrib/link/frameworks/bootstrap5.py b/djangocms_frontend/contrib/link/frameworks/bootstrap5.py index 1123d0c4..6dab2a29 100644 --- a/djangocms_frontend/contrib/link/frameworks/bootstrap5.py +++ b/djangocms_frontend/contrib/link/frameworks/bootstrap5.py @@ -28,6 +28,5 @@ def render(self, context, instance, placeholder): link_classes.append("d-block") if instance.config.get("link_stretched", False): link_classes.append("stretched-link") - context["link"] = instance.get_link() instance.add_classes(link_classes) return super().render(context, instance, placeholder) diff --git a/djangocms_frontend/contrib/link/helpers.py b/djangocms_frontend/contrib/link/helpers.py index d71c587c..41d6aa8c 100644 --- a/djangocms_frontend/contrib/link/helpers.py +++ b/djangocms_frontend/contrib/link/helpers.py @@ -1,52 +1,10 @@ -from importlib import import_module - -from cms.forms.utils import get_page_choices -from cms.models import Page -from django.conf import settings as django_settings -from django.contrib.admin import site +from django.apps import apps from django.contrib.contenttypes.models import ContentType from django.contrib.sites.models import Site -from django.core.exceptions import FieldError, ObjectDoesNotExist -from django.utils.encoding import force_str +from django.core.exceptions import ObjectDoesNotExist from djangocms_frontend.helpers import get_related_object -LINK_MODELS = getattr(django_settings, "DJANGOCMS_FRONTEND_LINK_MODELS", []) - - -def create_querysets(link_models): - querysets = [] - for item in link_models: - if item["class_path"] != "cms.models.Page": - # CMS pages are collected using a cms function to preserve hierarchy - section = item["type"] - parts = item["class_path"].rsplit(".", 1) - cls = getattr(import_module(parts[0]), parts[1]) - queryset = cls.objects - - if "manager_method" in item: - queryset = getattr(queryset, item["manager_method"])() - - if "filter" in item: - for k, v in item["filter"].items(): - try: - # Attempt to execute any callables in the filter dict. - item["filter"][k] = v() - except TypeError: - # OK, it wasn't a callable, so, leave it be - pass - queryset = queryset.filter(**item["filter"]) - else: - if "manager_method" not in item: - queryset = queryset.all() - if "order_by" in item: - queryset = queryset.order_by(item["order_by"]) - querysets.append((section, queryset, item.get("search", None), cls)) - return querysets - - -_querysets = create_querysets(LINK_MODELS) - def get_object_for_value(value): if isinstance(value, str) and "-" in value: @@ -62,175 +20,17 @@ def get_object_for_value(value): return None -def unescape(text, nbsp): - return ( - text.replace(" ", nbsp) - .replace("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace(""", '"') - .replace("'", "'") - ) - - -def get_link_choices(request, term="", lang=None, nbsp=None): - global _querysets - - if nbsp is None: - nbsp = "" if term else "\u2000" - available_objects = [] - # Now create our list of cms pages - type_id = ContentType.objects.get_for_model(Page).id - for value, descr in get_page_choices(lang): - if isinstance(descr, list): - available_objects.append( - { - "text": value, - "children": [ - dict( - id=f"{type_id}-{page}", - # django admin's autocomplete view requires unescaped strings - # get_page_choices escapes strings, so we undo the escape - text=unescape(name, nbsp), - ) - for page, name in descr - if not isinstance(term, str) or term.upper() in name.upper() - ], - } - ) - elif value and isinstance(value, int): - available_objects.append(dict(id=f"{type_id}-{value}")) - - # Add list of additional non-cms pages - for section, qs, search, cls in _querysets: - objects = None - model_admin = site._registry.get(cls, None) - if search: - try: - objects = qs.filter(**{search + "__icontains": term}) - except FieldError: - pass - if objects is None: - objects = [item for item in qs.all() if (not isinstance(term, str)) or term.upper() in str(item).upper()] - if objects: - type_class = ContentType.objects.get_for_model(objects[0].__class__) - available_objects.append( - { - "text": force_str(section), - "children": [ - dict(id=f"{type_class.id}-{obj.id}", text=str(obj)) - for obj in objects - if request is None or model_admin and model_admin.has_view_permission(request, obj=obj) - ], - } - ) - return available_objects - - -def get_choices(request, term="", lang=None) -> list: - def to_choices(json): - return list( - (elem["text"], to_choices(elem["children"])) if "children" in elem else (elem["id"], elem["text"]) - for elem in json - ) - - return to_choices(get_link_choices(request, term, lang, " ")) - - class GetLinkMixin: - def __init__(self, *args, **kwargs): - self._cms_page = None - super().__init__(*args, **kwargs) - - def get_link(self): - if getattr(self, "url_grouper", None): + def get_link(self) -> str: + if "url_grouper" in self.config and self.config["url_grouper"] and apps.is_installed("djangocms_url_manager"): url_grouper = get_related_object(self.config, "url_grouper") if not url_grouper: return "" - # The next line is a workaround, since djangocms-url-manager does not provide a way of - # getting the current URL object. from djangocms_url_manager.models import Url - url = Url._base_manager.filter(url_grouper=url_grouper).order_by("pk").last() + url = Url.objects.filter(url_grouper=url_grouper).order_by("pk").last() if not url: # pragma: no cover return "" - # simulate the call to the unauthorized CMSPlugin.page property - cms_page = self.placeholder.page if self.placeholder_id else None - - # first, we check if the placeholder the plugin is attached to - # has a page. Thus, the check "is not None": - if cms_page is not None: - if getattr(cms_page, "node", None): - cms_page_site_id = getattr(cms_page.node, "site_id", None) - else: - cms_page_site_id = getattr(cms_page, "site_id", None) - # a plugin might not be attached to a page and thus has no site - # associated with it. This also applies to plugins inside - # static placeholders - else: - cms_page_site_id = None - return url.get_url(cms_page_site_id) or "" - - if getattr(self, "internal_link", None): - try: - ref_page = get_related_object(self.config, "internal_link") - link = ref_page.get_absolute_url() - except ( - KeyError, - TypeError, - ValueError, - AttributeError, - ObjectDoesNotExist, - ): - self.internal_link = None - return "" - - # simulate the call to the unauthorized CMSPlugin.page property - cms_page = self._cms_page or self.placeholder.page if self.placeholder_id else None - - # first, we check if the placeholder the plugin is attached to - # has a page. Thus, the check "is not None": - if cms_page is not None: - if getattr(cms_page, "node", None): - cms_page_site_id = getattr(cms_page.node, "site_id", None) - else: - cms_page_site_id = getattr(cms_page, "site_id", None) - # a plugin might not be attached to a page and thus has no site - # associated with it. This also applies to plugins inside - # static placeholders - else: - cms_page_site_id = None - - # now we do the same for the reference page the plugin links to - # in order to compare them later - if getattr(ref_page, "node", None): - ref_page_site_id = ref_page.node.site_id - elif getattr(ref_page, "site_id", None): - ref_page_site_id = ref_page.site_id - # if no external reference is found the plugin links to the - # current page - else: - ref_page_site_id = Site.objects.get_current().pk - - if ref_page_site_id != cms_page_site_id: - ref_site = Site.objects._get_site_by_id(ref_page_site_id).domain - link = f"//{ref_site}{link}" - - elif getattr(self, "file_link", None): - link = getattr(get_related_object(self.config, "file_link"), "url", "") - - elif getattr(self, "external_link", None): - link = self.external_link - - elif getattr(self, "phone", None): - link = "tel:{}".format(self.phone.replace(" ", "")) - - elif getattr(self, "mailto", None): - link = f"mailto:{self.mailto}" - - else: - link = "" - - if (not getattr(self, "phone", None) and not getattr(self, "mailto", None)) and getattr(self, "anchor", None): - link += f"#{self.anchor}" + return url.get_absolute_url() or "" - return link + from djangocms_link.helpers import get_link as djangocms_link_get_link + return djangocms_link_get_link(self.config.get("link", {}), Site.objects.get_current().pk) or "" diff --git a/djangocms_frontend/contrib/link/views.py b/djangocms_frontend/contrib/link/views.py deleted file mode 100644 index 87102528..00000000 --- a/djangocms_frontend/contrib/link/views.py +++ /dev/null @@ -1,30 +0,0 @@ -from django.contrib.auth.mixins import LoginRequiredMixin -from django.http import JsonResponse -from django.views.generic import View - -from .helpers import get_link_choices - - -class AutocompleteJsonView(LoginRequiredMixin, View): - """Handle AutocompleteWidget's AJAX requests for data.""" - - paginate_by = 20 - admin_site = None - - def get(self, request, *args, **kwargs): - """ - Return a JsonResponse with search results of the form: - { - results: [{id: "123" text: "foo"}], - pagination: {more: true} - } - """ - - self.term = kwargs.get("term", request.GET.get("term", "")).strip() - results = get_link_choices(request, self.term) - return JsonResponse( - { - "results": results, - "pagination": {"more": False}, - } - ) diff --git a/djangocms_frontend/contrib/listgroup/forms.py b/djangocms_frontend/contrib/listgroup/forms.py index cb97cd24..cecf785d 100644 --- a/djangocms_frontend/contrib/listgroup/forms.py +++ b/djangocms_frontend/contrib/listgroup/forms.py @@ -5,12 +5,7 @@ from djangocms_frontend import settings from ...common import MarginFormMixin, PaddingFormMixin, ResponsiveFormMixin -from ...fields import ( - AttributesFormField, - ButtonGroup, - ColoredButtonGroup, - TagTypeFormField, -) +from ...fields import AttributesFormField, ButtonGroup, ColoredButtonGroup, TagTypeFormField from ...models import FrontendUIItem from .constants import LISTGROUP_STATE_CHOICES diff --git a/djangocms_frontend/contrib/navigation/cms_plugins.py b/djangocms_frontend/contrib/navigation/cms_plugins.py index fb98a07b..784e5f6d 100644 --- a/djangocms_frontend/contrib/navigation/cms_plugins.py +++ b/djangocms_frontend/contrib/navigation/cms_plugins.py @@ -176,7 +176,8 @@ class NavLinkPlugin( - `module` (str): The module where the plugin belongs, displayed in the plugin list when editing a page. - `model` (Model): The Django model used to store the plugin's data. - `form` (Form): The form used to render the plugin's settings in the admin interface. - - `change_form_template` (str): The path to the template used to render the plugin's change form in the admin interface. + - `change_form_template` (str): The path to the template used to render the plugin's change form in the admin + interface. - `allow_children` (bool): Whether the plugin allows having child plugins. - `parent_classes` (list): List of parent plugin classes that this plugin can be nested within. - `child_classes` (list): List of child plugin classes that can be nested within this plugin. diff --git a/djangocms_frontend/contrib/navigation/forms.py b/djangocms_frontend/contrib/navigation/forms.py index 67459dd3..2ebf6f64 100644 --- a/djangocms_frontend/contrib/navigation/forms.py +++ b/djangocms_frontend/contrib/navigation/forms.py @@ -6,12 +6,7 @@ from djangocms_frontend.common import BackgroundFormMixin from djangocms_frontend.contrib import navigation from djangocms_frontend.contrib.link.forms import AbstractLinkForm, LinkForm -from djangocms_frontend.fields import ( - AttributesFormField, - ButtonGroup, - IconGroup, - TemplateChoiceMixin, -) +from djangocms_frontend.fields import AttributesFormField, ButtonGroup, IconGroup, TemplateChoiceMixin from djangocms_frontend.helpers import first_choice from djangocms_frontend.models import FrontendUIItem from djangocms_frontend.settings import NAVBAR_DESIGNS diff --git a/djangocms_frontend/contrib/navigation/templates/djangocms_frontend/bootstrap5/navigation/offcanvas/brand.html b/djangocms_frontend/contrib/navigation/templates/djangocms_frontend/bootstrap5/navigation/offcanvas/brand.html index 1cd8da16..90f2d5c5 100644 --- a/djangocms_frontend/contrib/navigation/templates/djangocms_frontend/bootstrap5/navigation/offcanvas/brand.html +++ b/djangocms_frontend/contrib/navigation/templates/djangocms_frontend/bootstrap5/navigation/offcanvas/brand.html @@ -1,4 +1,4 @@ -{% load cms_tags sekizai_tags %}{% spaceless %}{% with link=instance.get_link %}{% if link %}{% else %} <{{ instance.tag_type }}{{ instance.get_attributes }}>{% endif %} +{% load cms_tags sekizai_tags %}{% spaceless %}{% with link=instance.get_link %}{% if link %}{% else %}<{{ instance.tag_type }}{{ instance.get_attributes }}>{% endif %} {% for plugin in instance.child_plugin_instances %} {% with parentloop=forloop parent=instance %}{% render_plugin plugin %}{% endwith %} {% empty %}{{ instance.simple_content }}{% endfor %} diff --git a/djangocms_frontend/contrib/tabs/forms.py b/djangocms_frontend/contrib/tabs/forms.py index 29dd825b..5aaaefc3 100644 --- a/djangocms_frontend/contrib/tabs/forms.py +++ b/djangocms_frontend/contrib/tabs/forms.py @@ -4,21 +4,10 @@ from ... import settings from ...common import PaddingFormMixin -from ...fields import ( - AttributesFormField, - ButtonGroup, - IconGroup, - TagTypeFormField, - TemplateChoiceMixin, -) +from ...fields import AttributesFormField, ButtonGroup, IconGroup, TagTypeFormField, TemplateChoiceMixin from ...helpers import first_choice from ...models import FrontendUIItem -from .constants import ( - TAB_ALIGNMENT_CHOICES, - TAB_EFFECT_CHOICES, - TAB_TEMPLATE_CHOICES, - TAB_TYPE_CHOICES, -) +from .constants import TAB_ALIGNMENT_CHOICES, TAB_EFFECT_CHOICES, TAB_TEMPLATE_CHOICES, TAB_TYPE_CHOICES class TabForm(TemplateChoiceMixin, EntangledModelForm): diff --git a/djangocms_frontend/contrib/utilities/forms.py b/djangocms_frontend/contrib/utilities/forms.py index 549cb2b4..8d7f2e0e 100644 --- a/djangocms_frontend/contrib/utilities/forms.py +++ b/djangocms_frontend/contrib/utilities/forms.py @@ -5,13 +5,7 @@ from ... import settings from ...common import SpacingFormMixin -from ...fields import ( - AttributesFormField, - ButtonGroup, - ColoredButtonGroup, - IconGroup, - TagTypeFormField, -) +from ...fields import AttributesFormField, ButtonGroup, ColoredButtonGroup, IconGroup, TagTypeFormField from ...helpers import first_choice from ...models import FrontendUIItem from .. import utilities diff --git a/djangocms_frontend/helpers.py b/djangocms_frontend/helpers.py index dd007707..2fc07d11 100644 --- a/djangocms_frontend/helpers.py +++ b/djangocms_frontend/helpers.py @@ -33,6 +33,7 @@ def get_related_object(scope, field_name): class get_related: """Descriptor lazily getting related objects from the config dict.""" + def __init__(self, key): self.key = key @@ -74,12 +75,12 @@ def insert_fields(fieldsets, new_fields, block=None, position=-1, blockname=None modify[1]["fields"] = ( list(fields[: position + 1] if position != -1 else fields) + list(new_fields) - + list(fields[position + 1 :] if position != -1 else []) + + list(fields[position + 1:] if position != -1 else []) ) fs = ( list(fieldsets[:block] if block != -1 else fieldsets) + [modify] - + list(fieldsets[block + 1 :] if block != -1 else []) + + list(fieldsets[block + 1:] if block != -1 else []) ) return fs @@ -165,6 +166,7 @@ class FrontendEditableAdminMixin: in the frontend by double-clicking on fields rendered with the ``render_model`` template tag. """ + frontend_editable_fields = [] def get_urls(self): # pragma: no cover @@ -177,7 +179,7 @@ def pat(regex, fn): return re_path(regex, self.admin_site.admin_view(fn), name=f"{info}_{fn.__name__}") url_patterns = [ - pat(r'edit-field/(%s)/([a-z\-]+)/$' % SLUG_REGEXP, self.edit_field), + pat(r"edit-field/(%s)/([a-z\-]+)/$" % SLUG_REGEXP, self.edit_field), ] return url_patterns + super().get_urls() @@ -203,21 +205,15 @@ def edit_field(self, request, object_id, language): raw_fields = request.GET.get("edit_fields") fields = [field for field in raw_fields.split(",") if field in self.frontend_editable_fields] if not fields: - context = { - 'opts': opts, - 'message': _("Field %s not found") % raw_fields - } - return render(request, 'admin/cms/page/plugin/error_form.html', context) + context = {"opts": opts, "message": _("Field %s not found") % raw_fields} + return render(request, "admin/cms/page/plugin/error_form.html", context) if not request.user.has_perm(f"{self.model._meta.app_label}.change_{self.model._meta.model_name}"): - context = { - 'opts': opts, - 'message': _("You do not have permission to edit this item") - } - return render(request, 'admin/cms/page/plugin/error_form.html', context) + context = {"opts": opts, "message": _("You do not have permission to edit this item")} + return render(request, "admin/cms/page/plugin/error_form.html", context) # Dynamically creates the form class with only `field_name` field # enabled form_class = self.get_form(request, obj, fields=fields) - if not cancel_clicked and request.method == 'POST': + if not cancel_clicked and request.method == "POST": form = form_class(instance=obj, data=request.POST) if form.is_valid(): new_object = form.save(commit=False) @@ -225,36 +221,37 @@ def edit_field(self, request, object_id, language): saved_successfully = True else: form = form_class(instance=obj) - admin_form = AdminForm(form, fieldsets=[(None, {'fields': fields})], prepopulated_fields={}, - model_admin=self) + admin_form = AdminForm(form, fieldsets=[(None, {"fields": fields})], prepopulated_fields={}, model_admin=self) media = self.media + admin_form.media context = { - 'CMS_MEDIA_URL': get_cms_setting('MEDIA_URL'), - 'title': opts.verbose_name, - 'plugin': None, - 'plugin_id': None, - 'adminform': admin_form, - 'add': False, - 'is_popup': True, - 'media': media, - 'opts': opts, - 'change': True, - 'save_as': False, - 'has_add_permission': False, - 'window_close_timeout': 10, + "CMS_MEDIA_URL": get_cms_setting("MEDIA_URL"), + "title": opts.verbose_name, + "plugin": None, + "plugin_id": None, + "adminform": admin_form, + "add": False, + "is_popup": True, + "media": media, + "opts": opts, + "change": True, + "save_as": False, + "has_add_permission": False, + "window_close_timeout": 10, } if cancel_clicked: # cancel button was clicked - context.update({ - 'cancel': True, - }) - return render(request, 'admin/cms/page/plugin/confirm_form.html', context) - if not cancel_clicked and request.method == 'POST' and saved_successfully: + context.update( + { + "cancel": True, + } + ) + return render(request, "admin/cms/page/plugin/confirm_form.html", context) + if not cancel_clicked and request.method == "POST" and saved_successfully: if isinstance(self, CMSPluginBase): - if hasattr(obj.placeholder, 'mark_as_dirty'): + if hasattr(obj.placeholder, "mark_as_dirty"): # Only relevant for v3: mark the placeholder as dirty so user can publish changes obj.placeholder.mark_as_dirty(obj.language, clear_cache=False) # Update the structure board by populating the data bridge return self.render_close_frame(request, obj) - render(request, 'admin/cms/page/plugin/confirm_form.html', context) - return render(request, 'admin/cms/page/plugin/change_form.html', context) + return render(request, "admin/cms/page/plugin/confirm_form.html", context) + return render(request, "admin/cms/page/plugin/change_form.html", context) diff --git a/djangocms_frontend/management/bootstrap4_migration.py b/djangocms_frontend/management/bootstrap4_migration.py index 7db2b1c6..fcdf20cf 100644 --- a/djangocms_frontend/management/bootstrap4_migration.py +++ b/djangocms_frontend/management/bootstrap4_migration.py @@ -305,7 +305,7 @@ def replace(item, old, new): def replace_left(item, old, new): if item[: len(old)] == old: - return new + item[len(old) :] + return new + item[len(old):] return item classes = new_obj.attributes["class"].split() @@ -361,7 +361,7 @@ def a001_alignment(obj, new_obj, field): new_obj.config[field].replace("text-right", "end") -def m001_spacing_mixin(obj, new_obj, type): +def m001_spacing_mixin(obj, new_obj, type): # noqa: A002 classes = new_obj.config["attributes"].get("class", "").split() if classes: for size, _ in list(settings.SPACER_SIZE_CHOICES) + ([("auto", "auto")] if type == "margin" else []): @@ -404,7 +404,7 @@ def m002_responsive_mixin(obj, new_obj): visible = False hit = True classes.remove(f"{stump}{hidden}") - for type in display: + for type in display: # noqa: A001 if f"{stump}{type}" in classes and not visible: visible = True hit = True diff --git a/djangocms_frontend/migrations/0001_initial.py b/djangocms_frontend/migrations/0001_initial.py index a9588ed0..c566543e 100644 --- a/djangocms_frontend/migrations/0001_initial.py +++ b/djangocms_frontend/migrations/0001_initial.py @@ -2,16 +2,10 @@ import django.core.serializers.json import django.db.models.deletion -from cms.utils.compat import DJANGO_3_1 from django.db import migrations, models import djangocms_frontend.fields -if DJANGO_3_1: - from django_jsonfield_backport.models import JSONField -else: - JSONField = models.JSONField - class Migration(migrations.Migration): initial = True @@ -49,7 +43,7 @@ class Migration(migrations.Migration): ), ( "config", - JSONField( + models.JSONField( default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder, ), diff --git a/djangocms_frontend/migrations/0002_migrate_links.py b/djangocms_frontend/migrations/0002_migrate_links.py new file mode 100644 index 00000000..a125f8c1 --- /dev/null +++ b/djangocms_frontend/migrations/0002_migrate_links.py @@ -0,0 +1,85 @@ +from django.db import migrations + + +def convert_item(config, direction): + """Convert FrontendUIItem config to new djangocms_link format.""" + if direction == "forward": + if config.get("external_link"): + if (config.get("anchor") or "").strip(): + anchor = "#" + config.get("anchor").strip() + del config["anchor"] + else: + anchor = "" + config["link"] = {"external_link": config.get("external_link") + anchor} + del config["external_link"] + return True + if config.get("internal_link"): + model = config.get("internal_link").get("model") + pk = config.get("internal_link").get("pk") + config["link"] = {"internal_link": f"{model}:{pk}"} + if config.get("anchor"): + config["link"]["anchor"] = "#" + config.get("anchor") + del config["anchor"] + del config["internal_link"] + return True + if config.get("file_link"): + config["link"] = {"file_link": config.get("file_link").get("pk")} + del config["file_link"] + return True + if config.get("phone"): + config["link"] = {"external_link": f"tel:{config.get('phone')}"} + del config["phone"] + return True + if config.get("mailto"): + config["link"] = {"external_link": f"mailto:{config.get('mailto')}"} + del config["mailto"] + return True + if config.get("anchor"): + config["link"] = {"external_link": "#" + config.get('anchor')} + del config["anchor"] + return True + + elif direction == "backward" and config.get("link"): + link = config.get("link") + if link.get("external_link"): + if "#" in link.get("external_link"): + config["anchor"] = link.get("external_link").split("#", 1)[1] + ext = link.get("external_link").split("#", 1)[0] + if ext.startswith("tel:"): + config["phone"] = ext[4:] + elif ext.startswith("mailto:"): + config["mailto"] = ext[7:] + elif ext: + config["external_link"] = ext + elif link.get("internal_link"): + model, pk = link.get("internal_link").split(":") + config["internal_link"] = {"model": model, "pk": int(pk)} + if link.get("anchor"): + config["anchor"] = link.get("anchor")[1:] + elif link.get("file_link"): + config["file_link"] = {"model": "filer.file", "pk": int(link.get("file_link"))} + del config["link"] + return True + return False + + +def convert(apps, schema_editor, direction): + FrontendUIItem = apps.get_model("djangocms_frontend", "FrontendUIItem") + for item in FrontendUIItem.objects.all(): + changed = convert_item(item.config, direction) + if changed: + item.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("djangocms_frontend", "0001_initial"), + ] + + operations = [ + migrations.RunPython( + lambda apps, schema_editor: convert(apps, schema_editor, "forward"), + lambda apps, schema_editor: convert(apps, schema_editor, "backward"), + elidable=True + ), + ] diff --git a/djangocms_frontend/models.py b/djangocms_frontend/models.py index 64e05afd..3239f292 100644 --- a/djangocms_frontend/models.py +++ b/djangocms_frontend/models.py @@ -99,7 +99,7 @@ def initialize_from_form(self, form=None): if not getattr(form._meta, "model", None): form._meta.model = self.__class__ form = form() # instantiate - entangled_fields = getattr(getattr(form, "Meta", None), "entangled_fields", {}).get("config", ()) + entangled_fields = getattr(getattr(form, "_meta", None), "entangled_fields", {}).get("config", ()) for field in entangled_fields: self.config.setdefault(field, {} if field == "attributes" else form[field].initial or "") return self diff --git a/djangocms_frontend/plugin_tag.py b/djangocms_frontend/plugin_tag.py new file mode 100644 index 00000000..c1024d32 --- /dev/null +++ b/djangocms_frontend/plugin_tag.py @@ -0,0 +1,97 @@ +import copy +import importlib +import warnings + +from cms.plugin_pool import plugin_pool +from cms.templatetags.cms_tags import render_plugin +from django.conf import settings +from django.contrib.admin.sites import site as admin_site +from django.template import engines +from django.template.library import SimpleNode +from django.template.loader import get_template + +django_engine = engines["django"] + +plugin_tag_pool = {} + + +IGNORED_FIELDS = ( + "id", + "cmsplugin_ptr", + "language", + "plugin_type", + "position", + "creation_date", + "ui_item", +) + +allowed_plugin_types = tuple( + getattr(importlib.import_module(cls.rsplit(".", 1)[0]), cls.rsplit(".", 1)[-1]) if isinstance(cls, str) else cls + for cls in getattr(settings, "CMS_COMPONENT_PLUGINS", []) +) + + +def _get_plugindefaults(instance): + defaults = { + field.name: getattr(instance, field.name) + for field in instance._meta.fields + if field.name not in IGNORED_FIELDS and bool(getattr(instance, field.name)) + } + defaults["plugin_type"] = instance.__class__.__name__ + return defaults + + +class _DummyUser: + is_superuser = True + is_staff = True + + +class _DummyRequest: + user = _DummyUser() + + +def render_dummy_plugin(context, dummy_plugin): + return dummy_plugin.nodelist.render(context) + + +def patch_template(template): + """Patches the template to use the dummy plugin renderer instead of the real one.""" + copied_template = copy.deepcopy(template) + patch = False + for node in copied_template.template.nodelist.get_nodes_by_type(SimpleNode): + if node.func == render_plugin: + patch = True + node.func = render_dummy_plugin + return copied_template if patch else template + + +def setup(): + global plugin_tag_pool + + for plugin in plugin_pool.get_all_plugins(): + if not issubclass(plugin, allowed_plugin_types): + continue + tag_name = plugin.__name__.lower() + if tag_name.endswith("plugin"): + tag_name = tag_name[:-6] + try: + instance = plugin.model() # Create instance with defaults + plugin_admin = plugin(admin_site=admin_site) + if hasattr(instance, "initialize_from_form"): + instance.initialize_from_form(plugin.form) + if tag_name not in plugin_tag_pool: + template = get_template(plugin_admin._get_render_template({"request": None}, instance, None)) + plugin_tag_pool[tag_name] = { + "defaults": { + **_get_plugindefaults(instance), + **dict(plugin_type=plugin.__name__), + }, + "template": patch_template(template), + "class": plugin, + } + else: # pragma: no cover + warnings.warn( + f"Duplicate candidates for {{% plugin \"{tag_name}\" %}} found. " + f"Only registered {plugin_tag_pool[tag_name]['class'].__name__}.", stacklevel=1) + except Exception as exc: # pragma: no cover + warnings.warn(f"{plugin.__name__}: \n{str(exc)}", stacklevel=1) diff --git a/djangocms_frontend/static/djangocms_frontend/css/base.css b/djangocms_frontend/static/djangocms_frontend/css/base.css index d73825f1..e86c384f 100644 --- a/djangocms_frontend/static/djangocms_frontend/css/base.css +++ b/djangocms_frontend/static/djangocms_frontend/css/base.css @@ -5,4 +5,4 @@ /private/sass instead */ -@charset "UTF-8";:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:33,37,41;--bs-body-bg-rgb:255,255,255;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:0.8125rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-2xl:2rem;--bs-border-radius-pill:50rem;--bs-link-color:#0d6efd;--bs-link-hover-color:#0a58ca;--bs-code-color:#d63384;--bs-highlight-bg:#fff3cd}.btn{--bs-btn-padding-x:0.75rem;--bs-btn-padding-y:0.375rem;--bs-btn-font-size:0.8125rem;--bs-btn-font-weight:400;--bs-btn-line-height:1.5;--bs-btn-color:#212529;--bs-btn-bg:transparent;--bs-btn-border-width:1px;--bs-btn-border-color:transparent;--bs-btn-border-radius:0.375rem;--bs-btn-hover-border-color:transparent;--bs-btn-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.15),0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity:0.65;--bs-btn-focus-box-shadow:0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-ms-user-select:none;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,.btn.active,.btn.show,.btn:first-child:active,:not(.btn-check)+.btn:active{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible,.btn:first-child:active:focus-visible,:not(.btn-check)+.btn:active:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0b5ed7;--bs-btn-hover-border-color:#0a58ca;--bs-btn-focus-shadow-rgb:49,132,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0a58ca;--bs-btn-active-border-color:#0a53be;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#0d6efd;--bs-btn-disabled-border-color:#0d6efd}.btn-secondary{--bs-btn-color:#fff;--bs-btn-bg:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#5c636a;--bs-btn-hover-border-color:#565e64;--bs-btn-focus-shadow-rgb:130,138,145;--bs-btn-active-color:#fff;--bs-btn-active-bg:#565e64;--bs-btn-active-border-color:#51585e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#6c757d;--bs-btn-disabled-border-color:#6c757d}.btn-success{--bs-btn-color:#fff;--bs-btn-bg:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#157347;--bs-btn-hover-border-color:#146c43;--bs-btn-focus-shadow-rgb:60,153,110;--bs-btn-active-color:#fff;--bs-btn-active-bg:#146c43;--bs-btn-active-border-color:#13653f;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#198754;--bs-btn-disabled-border-color:#198754}.btn-info{--bs-btn-color:#000;--bs-btn-bg:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#31d2f2;--bs-btn-hover-border-color:#25cff2;--bs-btn-focus-shadow-rgb:11,172,204;--bs-btn-active-color:#000;--bs-btn-active-bg:#3dd5f3;--bs-btn-active-border-color:#25cff2;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#0dcaf0;--bs-btn-disabled-border-color:#0dcaf0}.btn-warning{--bs-btn-color:#000;--bs-btn-bg:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffca2c;--bs-btn-hover-border-color:#ffc720;--bs-btn-focus-shadow-rgb:217,164,6;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffcd39;--bs-btn-active-border-color:#ffc720;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#ffc107;--bs-btn-disabled-border-color:#ffc107}.btn-danger{--bs-btn-color:#fff;--bs-btn-bg:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#bb2d3b;--bs-btn-hover-border-color:#b02a37;--bs-btn-focus-shadow-rgb:225,83,97;--bs-btn-active-color:#fff;--bs-btn-active-bg:#b02a37;--bs-btn-active-border-color:#a52834;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#dc3545;--bs-btn-disabled-border-color:#dc3545}.btn-light{--bs-btn-color:#000;--bs-btn-bg:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#d3d4d5;--bs-btn-hover-border-color:#c6c7c8;--bs-btn-focus-shadow-rgb:211,212,213;--bs-btn-active-color:#000;--bs-btn-active-bg:#c6c7c8;--bs-btn-active-border-color:#babbbc;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#f8f9fa;--bs-btn-disabled-border-color:#f8f9fa}.btn-dark{--bs-btn-color:#fff;--bs-btn-bg:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#424649;--bs-btn-hover-border-color:#373b3e;--bs-btn-focus-shadow-rgb:66,70,73;--bs-btn-active-color:#fff;--bs-btn-active-bg:#4d5154;--bs-btn-active-border-color:#373b3e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#212529;--bs-btn-disabled-border-color:#212529}.btn-outline-primary{--bs-btn-color:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0d6efd;--bs-btn-hover-border-color:#0d6efd;--bs-btn-focus-shadow-rgb:13,110,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0d6efd;--bs-btn-active-border-color:#0d6efd;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0d6efd;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0d6efd;--bs-gradient:none}.btn-outline-secondary{--bs-btn-color:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6c757d;--bs-btn-hover-border-color:#6c757d;--bs-btn-focus-shadow-rgb:108,117,125;--bs-btn-active-color:#fff;--bs-btn-active-bg:#6c757d;--bs-btn-active-border-color:#6c757d;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#6c757d;--bs-gradient:none}.btn-outline-success{--bs-btn-color:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#198754;--bs-btn-hover-border-color:#198754;--bs-btn-focus-shadow-rgb:25,135,84;--bs-btn-active-color:#fff;--bs-btn-active-bg:#198754;--bs-btn-active-border-color:#198754;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#198754;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#198754;--bs-gradient:none}.btn-outline-info{--bs-btn-color:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#0dcaf0;--bs-btn-hover-border-color:#0dcaf0;--bs-btn-focus-shadow-rgb:13,202,240;--bs-btn-active-color:#000;--bs-btn-active-bg:#0dcaf0;--bs-btn-active-border-color:#0dcaf0;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0dcaf0;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0dcaf0;--bs-gradient:none}.btn-outline-warning{--bs-btn-color:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffc107;--bs-btn-hover-border-color:#ffc107;--bs-btn-focus-shadow-rgb:255,193,7;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffc107;--bs-btn-active-border-color:#ffc107;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#ffc107;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#ffc107;--bs-gradient:none}.btn-outline-danger{--bs-btn-color:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#dc3545;--bs-btn-hover-border-color:#dc3545;--bs-btn-focus-shadow-rgb:220,53,69;--bs-btn-active-color:#fff;--bs-btn-active-bg:#dc3545;--bs-btn-active-border-color:#dc3545;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#dc3545;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#dc3545;--bs-gradient:none}.btn-outline-light{--bs-btn-color:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#f8f9fa;--bs-btn-hover-border-color:#f8f9fa;--bs-btn-focus-shadow-rgb:248,249,250;--bs-btn-active-color:#000;--bs-btn-active-bg:#f8f9fa;--bs-btn-active-border-color:#f8f9fa;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#f8f9fa;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#f8f9fa;--bs-gradient:none}.btn-outline-dark{--bs-btn-color:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#212529;--bs-btn-hover-border-color:#212529;--bs-btn-focus-shadow-rgb:33,37,41;--bs-btn-active-color:#fff;--bs-btn-active-bg:#212529;--bs-btn-active-border-color:#212529;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#212529;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#212529;--bs-gradient:none}.btn-link{--bs-btn-font-weight:400;--bs-btn-color:var(--bs-link-color);--bs-btn-bg:transparent;--bs-btn-border-color:transparent;--bs-btn-hover-color:var(--bs-link-hover-color);--bs-btn-hover-border-color:transparent;--bs-btn-active-color:var(--bs-link-hover-color);--bs-btn-active-border-color:transparent;--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-border-color:transparent;--bs-btn-box-shadow:none;--bs-btn-focus-shadow-rgb:49,132,253;text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-lg{--bs-btn-padding-y:0.5rem;--bs-btn-padding-x:1rem;--bs-btn-font-size:1.015625rem;--bs-btn-border-radius:0.5rem}.btn-sm{--bs-btn-padding-y:0.25rem;--bs-btn-padding-x:0.5rem;--bs-btn-font-size:0.7109375rem;--bs-btn-border-radius:0.25rem}.link-primary{color:#0d6efd!important}.link-primary:focus,.link-primary:hover{color:#0a58ca!important}.link-secondary{color:#6c757d!important}.link-secondary:focus,.link-secondary:hover{color:#565e64!important}.link-success{color:#198754!important}.link-success:focus,.link-success:hover{color:#146c43!important}.link-info{color:#0dcaf0!important}.link-info:focus,.link-info:hover{color:#3dd5f3!important}.link-warning{color:#ffc107!important}.link-warning:focus,.link-warning:hover{color:#ffcd39!important}.link-danger{color:#dc3545!important}.link-danger:focus,.link-danger:hover{color:#b02a37!important}.link-light{color:#f8f9fa!important}.link-light:focus,.link-light:hover{color:#f9fafb!important}.link-dark{color:#212529!important}.link-dark:focus,.link-dark:hover{color:#1a1e21!important}.nav{--bs-nav-link-padding-x:1rem;--bs-nav-link-padding-y:0.3rem;--bs-nav-link-color:var(--bs-link-color);--bs-nav-link-hover-color:var(--bs-link-hover-color);--bs-nav-link-disabled-color:#6c757d;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:var(--bs-nav-link-hover-color)}.nav-link.disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width:1px;--bs-nav-tabs-border-color:#dee2e6;--bs-nav-tabs-border-radius:0.375rem;--bs-nav-tabs-link-hover-border-color:#e9ecef #e9ecef #dee2e6;--bs-nav-tabs-link-active-color:#495057;--bs-nav-tabs-link-active-bg:#fff;--bs-nav-tabs-link-active-border-color:#dee2e6 #dee2e6 #fff;border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--bs-nav-tabs-border-width));background:0 0;border:var(--bs-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-link.disabled,.nav-tabs .nav-link:disabled{color:var(--bs-nav-link-disabled-color);background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1 * var(--bs-nav-tabs-border-width));border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--bs-nav-pills-border-radius:4px;--bs-nav-pills-link-active-color:#fff;--bs-nav-pills-link-active-bg:#0bf}.nav-pills .nav-link{background:0 0;border:0;border-radius:var(--bs-nav-pills-border-radius)}.nav-pills .nav-link:disabled{color:var(--bs-nav-link-disabled-color);background-color:transparent;border-color:transparent}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-fill .nav-item,.nav-fill>.nav-link{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.icon{display:inline-block;vertical-align:top;width:1em;height:1em;background-position:center;background-repeat:no-repeat}.icon svg{display:block;width:100%;height:100%}.icon-info{width:.9em;font-size:110%!important}.icon-white{color:#fff}.icon-white svg{fill:#fff}.icon-black{color:#000}.icon-black svg{fill:#000}.icon-primary{color:#0bf}.icon-primary svg{fill:#0bf}.djangocms-icon .icon>input{float:left;position:relative;top:12px}.djangocms-icon .caret{margin-inline-start:8px}.aligned .frontend-button-group label{min-width:unset}.frontend-button-group{display:inline-block}.frontend-button-group .btn{box-sizing:border-box;cursor:pointer;-webkit-appearance:none;margin:2px;overflow:hidden;text-overflow:ellipsis}.frontend-button-group .btn.active{outline:3px solid #0bf;border-color:#fff!important}.frontend-button-group .btn-default.active{border-radius:0;background-color:#0bf!important}.frontend-button-group-context-colors>div,.frontend-button-group-context-size>div{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;min-height:75px}.frontend-button-group-context-colors .btn{-ms-flex-preferred-size:calc(25% - 4px);flex-basis:calc(25% - 4px)}@media (min-width:820px){.frontend-button-group-context-colors .btn{-ms-flex-preferred-size:calc(20% - 4px);flex-basis:calc(20% - 4px)}}.frontend-button-group-icons .icon,.frontend-grid-icons .icon{font-size:24px}.frontend-button-group-icons .icon-flex-align-center,.frontend-button-group-icons .icon-flex-align-end,.frontend-button-group-icons .icon-flex-align-start,.frontend-grid-icons .icon-flex-align-center,.frontend-grid-icons .icon-flex-align-end,.frontend-grid-icons .icon-flex-align-start{transform:scale(1.4)}.frontend-button-group-icons .icon-flex-content-around,.frontend-button-group-icons .icon-flex-content-between,.frontend-grid-icons .icon-flex-content-around,.frontend-grid-icons .icon-flex-content-between{transform:scale(1.6)}.frontend-button-group-icons .icon-flex-self-center,.frontend-button-group-icons .icon-flex-self-end,.frontend-button-group-icons .icon-flex-self-start,.frontend-grid-icons .icon-flex-self-center,.frontend-grid-icons .icon-flex-self-end,.frontend-grid-icons .icon-flex-self-start{transform:scale(1.4)}.frontend-button-group-icons .icon-align-reset,.frontend-button-group-icons .icon-no-selection,.frontend-grid-icons .icon-align-reset,.frontend-grid-icons .icon-no-selection{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16))}.frontend-button-group-icons .icon-align-items-start,.frontend-button-group-icons .icon-flex-align-start,.frontend-grid-icons .icon-align-items-start,.frontend-grid-icons .icon-flex-align-start{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16));transform:scale(1.4)}.frontend-button-group-icons .icon-align-items-center,.frontend-button-group-icons .icon-flex-align-center,.frontend-grid-icons .icon-align-items-center,.frontend-grid-icons .icon-flex-align-center{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16));transform:scale(1.4)}.frontend-button-group-icons .icon-align-items-end,.frontend-button-group-icons .icon-flex-align-end,.frontend-grid-icons .icon-align-items-end,.frontend-grid-icons .icon-flex-align-end{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16));transform:scale(1.4)}.frontend-button-group-icons .icon-flex-content-start,.frontend-button-group-icons .icon-justify-content-start,.frontend-button-group-icons .icon-start,.frontend-grid-icons .icon-flex-content-start,.frontend-grid-icons .icon-justify-content-start,.frontend-grid-icons .icon-start{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16))}.frontend-button-group-icons .icon-center,.frontend-button-group-icons .icon-flex-content-center,.frontend-button-group-icons .icon-justify-content-center,.frontend-grid-icons .icon-center,.frontend-grid-icons .icon-flex-content-center,.frontend-grid-icons .icon-justify-content-center{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16))}.frontend-button-group-icons .icon-end,.frontend-button-group-icons .icon-flex-content-end,.frontend-button-group-icons .icon-justify-content-end,.frontend-grid-icons .icon-end,.frontend-grid-icons .icon-flex-content-end,.frontend-grid-icons .icon-justify-content-end{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16))}.frontend-button-group-icons .icon-flex-content-around,.frontend-button-group-icons .icon-justify-content-around,.frontend-grid-icons .icon-flex-content-around,.frontend-grid-icons .icon-justify-content-around{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16));transform:scale(1.6)}.frontend-button-group-icons .icon-flex-content-between,.frontend-button-group-icons .icon-justify-content-between,.frontend-grid-icons .icon-flex-content-between,.frontend-grid-icons .icon-justify-content-between{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16));transform:scale(1.6)}.frontend-button-group-icons .icon-nav-fill,.frontend-grid-icons .icon-nav-fill{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16));transform:scale(1.4)}.frontend-button-group-icons .icon-nav-justified,.frontend-grid-icons .icon-nav-justified{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16));transform:scale(1.4)}.frontend-button-group-icons .icon-flex-column,.frontend-grid-icons .icon-flex-column{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16));transform:scale(1.4)}.frontend-button-group-icons .icon-align-self-start,.frontend-button-group-icons .icon-flex-self-start,.frontend-grid-icons .icon-align-self-start,.frontend-grid-icons .icon-flex-self-start{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16));transform:scale(1.4)}.frontend-button-group-icons .icon-align-self-center,.frontend-button-group-icons .icon-flex-self-center,.frontend-grid-icons .icon-align-self-center,.frontend-grid-icons .icon-flex-self-center{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16));transform:scale(1.4)}.frontend-button-group-icons .icon-align-self-end,.frontend-button-group-icons .icon-flex-self-end,.frontend-grid-icons .icon-align-self-end,.frontend-grid-icons .icon-flex-self-end{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16));transform:scale(1.4)}.frontend-button-group-icons .icon-size-sm,.frontend-button-group-icons .icon-size-xs,.frontend-button-group-icons .icon-sm,.frontend-button-group-icons .icon-xs,.frontend-grid-icons .icon-size-sm,.frontend-grid-icons .icon-size-xs,.frontend-grid-icons .icon-sm,.frontend-grid-icons .icon-xs{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16))}.frontend-button-group-icons .icon-size-sm,.frontend-button-group-icons .icon-sm,.frontend-grid-icons .icon-size-sm,.frontend-grid-icons .icon-sm{transform:rotate(-90deg)}.frontend-button-group-icons .icon-md,.frontend-button-group-icons .icon-size-md,.frontend-grid-icons .icon-md,.frontend-grid-icons .icon-size-md{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16))}.frontend-button-group-icons .icon-lg,.frontend-button-group-icons .icon-size-lg,.frontend-grid-icons .icon-lg,.frontend-grid-icons .icon-size-lg{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16))}.frontend-button-group-icons .icon-size-xl,.frontend-button-group-icons .icon-xl,.frontend-grid-icons .icon-size-xl,.frontend-grid-icons .icon-xl{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16))}.frontend-button-group-icons .icon-size-xxl,.frontend-button-group-icons .icon-xxl,.frontend-grid-icons .icon-size-xxl,.frontend-grid-icons .icon-xxl{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16))}.frontend-button-group-icons .icon-mb,.frontend-grid-icons .icon-mb{transform:scale(1.3);background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 100))}.frontend-button-group-icons .icon-me,.frontend-grid-icons .icon-me{transform:scale(1.3);background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 100))}.frontend-button-group-icons .icon-ms,.frontend-grid-icons .icon-ms{transform:scale(1.3);background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 100))}.frontend-button-group-icons .icon-mt,.frontend-grid-icons .icon-mt{transform:scale(1.3);background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 100))}.frontend-button-group-icons .icon-mx,.frontend-grid-icons .icon-mx{transform:scale(1.3);background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 100))}.frontend-button-group-icons .icon-my,.frontend-grid-icons .icon-my{transform:scale(1.3);background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 100))}.frontend-button-group-icons .icon-pb,.frontend-grid-icons .icon-pb{transform:scale(1.5);background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 100))}.frontend-button-group-icons .icon-pe,.frontend-grid-icons .icon-pe{transform:scale(1.5);background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 100))}.frontend-button-group-icons .icon-ps,.frontend-grid-icons .icon-ps{transform:scale(1.5);background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 100))}.frontend-button-group-icons .icon-pt,.frontend-grid-icons .icon-pt{transform:scale(1.5);background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 100))}.frontend-button-group-icons .icon-px,.frontend-grid-icons .icon-px{transform:scale(1.5);background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 100))}.frontend-button-group-icons .icon-py,.frontend-grid-icons .icon-py{transform:scale(1.5);background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 100))}.icon-info{background-image:url('data:image/svg+xml;utf8,')}.module{margin:0 0 20px}.djangocms-frontend-row .form-row.field-create .icon{position:absolute;font-size:30px;margin-block-start:28px;margin-inline-start:4px}.djangocms-frontend-row .form-row.field-create input[name=create]{width:130px!important;padding-inline-end:5px!important;text-align:start}.djangocms-frontend-column .form-row.field-xs_col,.djangocms-frontend-column .form-row.field-xs_me,.djangocms-frontend-column .form-row.field-xs_ms,.djangocms-frontend-column .form-row.field-xs_offset,.djangocms-frontend-column .form-row.field-xs_order,.djangocms-frontend-row .form-row.field-row_cols_xs{position:relative;display:-ms-flexbox;display:flex;padding:0;min-width:800px}.djangocms-frontend-column .form-row.field-xs_col>div>div:not([class]),.djangocms-frontend-column .form-row.field-xs_me>div>div:not([class]),.djangocms-frontend-column .form-row.field-xs_ms>div>div:not([class]),.djangocms-frontend-column .form-row.field-xs_offset>div>div:not([class]),.djangocms-frontend-column .form-row.field-xs_order>div>div:not([class]),.djangocms-frontend-row .form-row.field-row_cols_xs>div>div:not([class]){width:unset!important;max-width:unset!important}.djangocms-frontend-column .form-row.field-xs_col .field-box:first-child,.djangocms-frontend-column .form-row.field-xs_me .field-box:first-child,.djangocms-frontend-column .form-row.field-xs_ms .field-box:first-child,.djangocms-frontend-column .form-row.field-xs_offset .field-box:first-child,.djangocms-frontend-column .form-row.field-xs_order .field-box:first-child,.djangocms-frontend-row .form-row.field-row_cols_xs .field-box:first-child{width:115px!important}.djangocms-frontend-column .form-row.field-xs_col .field-box,.djangocms-frontend-column .form-row.field-xs_col .fieldBox,.djangocms-frontend-column .form-row.field-xs_me .field-box,.djangocms-frontend-column .form-row.field-xs_me .fieldBox,.djangocms-frontend-column .form-row.field-xs_ms .field-box,.djangocms-frontend-column .form-row.field-xs_ms .fieldBox,.djangocms-frontend-column .form-row.field-xs_offset .field-box,.djangocms-frontend-column .form-row.field-xs_offset .fieldBox,.djangocms-frontend-column .form-row.field-xs_order .field-box,.djangocms-frontend-column .form-row.field-xs_order .fieldBox,.djangocms-frontend-row .form-row.field-row_cols_xs .field-box,.djangocms-frontend-row .form-row.field-row_cols_xs .fieldBox{position:relative;box-sizing:content-box;width:86px!important;-ms-flex:none;flex:none;display:block;padding:15px 10px;margin:0!important;border-bottom:1px solid #eee;float:left!important}.djangocms-frontend-column .form-row.field-xs_col .field-box input:not([type=checkbox]),.djangocms-frontend-column .form-row.field-xs_col .fieldBox input:not([type=checkbox]),.djangocms-frontend-column .form-row.field-xs_me .field-box input:not([type=checkbox]),.djangocms-frontend-column .form-row.field-xs_me .fieldBox input:not([type=checkbox]),.djangocms-frontend-column .form-row.field-xs_ms .field-box input:not([type=checkbox]),.djangocms-frontend-column .form-row.field-xs_ms .fieldBox input:not([type=checkbox]),.djangocms-frontend-column .form-row.field-xs_offset .field-box input:not([type=checkbox]),.djangocms-frontend-column .form-row.field-xs_offset .fieldBox input:not([type=checkbox]),.djangocms-frontend-column .form-row.field-xs_order .field-box input:not([type=checkbox]),.djangocms-frontend-column .form-row.field-xs_order .fieldBox input:not([type=checkbox]),.djangocms-frontend-row .form-row.field-row_cols_xs .field-box input:not([type=checkbox]),.djangocms-frontend-row .form-row.field-row_cols_xs .fieldBox input:not([type=checkbox]){text-align:end;padding-inline-end:5px!important;box-sizing:border-box;width:100%}.djangocms-frontend-column .form-row.field-xs_col .field-box label,.djangocms-frontend-column .form-row.field-xs_col .fieldBox label,.djangocms-frontend-column .form-row.field-xs_me .field-box label,.djangocms-frontend-column .form-row.field-xs_me .fieldBox label,.djangocms-frontend-column .form-row.field-xs_ms .field-box label,.djangocms-frontend-column .form-row.field-xs_ms .fieldBox label,.djangocms-frontend-column .form-row.field-xs_offset .field-box label,.djangocms-frontend-column .form-row.field-xs_offset .fieldBox label,.djangocms-frontend-column .form-row.field-xs_order .field-box label,.djangocms-frontend-column .form-row.field-xs_order .fieldBox label,.djangocms-frontend-row .form-row.field-row_cols_xs .field-box label,.djangocms-frontend-row .form-row.field-row_cols_xs .fieldBox label{font-size:12px!important;font-weight:400!important;color:#ccc!important;position:absolute;inset-inline-start:15px;inset-block-end:17px;text-transform:lowercase}.djangocms-frontend-column .form-row.field-xs_col .field-box .disabled,.djangocms-frontend-column .form-row.field-xs_col .fieldBox .disabled,.djangocms-frontend-column .form-row.field-xs_me .field-box .disabled,.djangocms-frontend-column .form-row.field-xs_me .fieldBox .disabled,.djangocms-frontend-column .form-row.field-xs_ms .field-box .disabled,.djangocms-frontend-column .form-row.field-xs_ms .fieldBox .disabled,.djangocms-frontend-column .form-row.field-xs_offset .field-box .disabled,.djangocms-frontend-column .form-row.field-xs_offset .fieldBox .disabled,.djangocms-frontend-column .form-row.field-xs_order .field-box .disabled,.djangocms-frontend-column .form-row.field-xs_order .fieldBox .disabled,.djangocms-frontend-row .form-row.field-row_cols_xs .field-box .disabled,.djangocms-frontend-row .form-row.field-row_cols_xs .fieldBox .disabled{color:#ccc;background:#eee}.djangocms-frontend-column .form-row.field-xs_col .field-box:last-child,.djangocms-frontend-column .form-row.field-xs_col .fieldBox:last-child,.djangocms-frontend-column .form-row.field-xs_me .field-box:last-child,.djangocms-frontend-column .form-row.field-xs_me .fieldBox:last-child,.djangocms-frontend-column .form-row.field-xs_ms .field-box:last-child,.djangocms-frontend-column .form-row.field-xs_ms .fieldBox:last-child,.djangocms-frontend-column .form-row.field-xs_offset .field-box:last-child,.djangocms-frontend-column .form-row.field-xs_offset .fieldBox:last-child,.djangocms-frontend-column .form-row.field-xs_order .field-box:last-child,.djangocms-frontend-column .form-row.field-xs_order .fieldBox:last-child,.djangocms-frontend-row .form-row.field-row_cols_xs .field-box:last-child,.djangocms-frontend-row .form-row.field-row_cols_xs .fieldBox:last-child{border-inline-end:none}.djangocms-frontend-column .form-row.field-xs_col .errors,.djangocms-frontend-column .form-row.field-xs_me .errors,.djangocms-frontend-column .form-row.field-xs_ms .errors,.djangocms-frontend-column .form-row.field-xs_offset .errors,.djangocms-frontend-column .form-row.field-xs_order .errors,.djangocms-frontend-row .form-row.field-row_cols_xs .errors{margin-bottom:0}.djangocms-frontend-column .form-row.field-xs_col .errorlist,.djangocms-frontend-column .form-row.field-xs_me .errorlist,.djangocms-frontend-column .form-row.field-xs_ms .errorlist,.djangocms-frontend-column .form-row.field-xs_offset .errorlist,.djangocms-frontend-column .form-row.field-xs_order .errorlist,.djangocms-frontend-row .form-row.field-row_cols_xs .errorlist{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.djangocms-frontend-column .form-row.field-xs_col.field-xs_me .field-box,.djangocms-frontend-column .form-row.field-xs_me.field-xs_me .field-box,.djangocms-frontend-column .form-row.field-xs_ms.field-xs_me .field-box,.djangocms-frontend-column .form-row.field-xs_offset.field-xs_me .field-box,.djangocms-frontend-column .form-row.field-xs_order.field-xs_me .field-box,.djangocms-frontend-row .form-row.field-row_cols_xs.field-xs_me .field-box{border-bottom:none}.djangocms-frontend-column .form-row.field-xs_col .field-box-label,.djangocms-frontend-column .form-row.field-xs_me .field-box-label,.djangocms-frontend-column .form-row.field-xs_ms .field-box-label,.djangocms-frontend-column .form-row.field-xs_offset .field-box-label,.djangocms-frontend-column .form-row.field-xs_order .field-box-label,.djangocms-frontend-row .form-row.field-row_cols_xs .field-box-label{display:-ms-flexbox;display:flex;margin-top:auto;color:#999}.djangocms-frontend-column .form-row.field-xs_col .field-box-label a,.djangocms-frontend-column .form-row.field-xs_me .field-box-label a,.djangocms-frontend-column .form-row.field-xs_ms .field-box-label a,.djangocms-frontend-column .form-row.field-xs_offset .field-box-label a,.djangocms-frontend-column .form-row.field-xs_order .field-box-label a,.djangocms-frontend-row .form-row.field-row_cols_xs .field-box-label a{width:100%;margin-top:auto;color:#999}.djangocms-frontend-column .form-row.field-xs_col .field-box-label a a,.djangocms-frontend-column .form-row.field-xs_me .field-box-label a a,.djangocms-frontend-column .form-row.field-xs_ms .field-box-label a a,.djangocms-frontend-column .form-row.field-xs_offset .field-box-label a a,.djangocms-frontend-column .form-row.field-xs_order .field-box-label a a,.djangocms-frontend-row .form-row.field-row_cols_xs .field-box-label a a{width:100%;margin-top:auto}.djangocms-frontend-column .form-row.field-xs_col .field-lg_me,.djangocms-frontend-column .form-row.field-xs_col .field-lg_ms,.djangocms-frontend-column .form-row.field-xs_col .field-md_me,.djangocms-frontend-column .form-row.field-xs_col .field-md_ms,.djangocms-frontend-column .form-row.field-xs_col .field-sm_me,.djangocms-frontend-column .form-row.field-xs_col .field-sm_ms,.djangocms-frontend-column .form-row.field-xs_col .field-xl_me,.djangocms-frontend-column .form-row.field-xs_col .field-xl_ms,.djangocms-frontend-column .form-row.field-xs_col .field-xs_me,.djangocms-frontend-column .form-row.field-xs_col .field-xs_ms,.djangocms-frontend-column .form-row.field-xs_col .field-xxl_me,.djangocms-frontend-column .form-row.field-xs_col .field-xxl_ms,.djangocms-frontend-column .form-row.field-xs_me .field-lg_me,.djangocms-frontend-column .form-row.field-xs_me .field-lg_ms,.djangocms-frontend-column .form-row.field-xs_me .field-md_me,.djangocms-frontend-column .form-row.field-xs_me .field-md_ms,.djangocms-frontend-column .form-row.field-xs_me .field-sm_me,.djangocms-frontend-column .form-row.field-xs_me .field-sm_ms,.djangocms-frontend-column .form-row.field-xs_me .field-xl_me,.djangocms-frontend-column .form-row.field-xs_me .field-xl_ms,.djangocms-frontend-column .form-row.field-xs_me .field-xs_me,.djangocms-frontend-column .form-row.field-xs_me .field-xs_ms,.djangocms-frontend-column .form-row.field-xs_me .field-xxl_me,.djangocms-frontend-column .form-row.field-xs_me .field-xxl_ms,.djangocms-frontend-column .form-row.field-xs_ms .field-lg_me,.djangocms-frontend-column .form-row.field-xs_ms .field-lg_ms,.djangocms-frontend-column .form-row.field-xs_ms .field-md_me,.djangocms-frontend-column .form-row.field-xs_ms .field-md_ms,.djangocms-frontend-column .form-row.field-xs_ms .field-sm_me,.djangocms-frontend-column .form-row.field-xs_ms .field-sm_ms,.djangocms-frontend-column .form-row.field-xs_ms .field-xl_me,.djangocms-frontend-column .form-row.field-xs_ms .field-xl_ms,.djangocms-frontend-column .form-row.field-xs_ms .field-xs_me,.djangocms-frontend-column .form-row.field-xs_ms .field-xs_ms,.djangocms-frontend-column .form-row.field-xs_ms .field-xxl_me,.djangocms-frontend-column .form-row.field-xs_ms .field-xxl_ms,.djangocms-frontend-column .form-row.field-xs_offset .field-lg_me,.djangocms-frontend-column .form-row.field-xs_offset .field-lg_ms,.djangocms-frontend-column .form-row.field-xs_offset .field-md_me,.djangocms-frontend-column .form-row.field-xs_offset .field-md_ms,.djangocms-frontend-column .form-row.field-xs_offset .field-sm_me,.djangocms-frontend-column .form-row.field-xs_offset .field-sm_ms,.djangocms-frontend-column .form-row.field-xs_offset .field-xl_me,.djangocms-frontend-column .form-row.field-xs_offset .field-xl_ms,.djangocms-frontend-column .form-row.field-xs_offset .field-xs_me,.djangocms-frontend-column .form-row.field-xs_offset .field-xs_ms,.djangocms-frontend-column .form-row.field-xs_offset .field-xxl_me,.djangocms-frontend-column .form-row.field-xs_offset .field-xxl_ms,.djangocms-frontend-column .form-row.field-xs_order .field-lg_me,.djangocms-frontend-column .form-row.field-xs_order .field-lg_ms,.djangocms-frontend-column .form-row.field-xs_order .field-md_me,.djangocms-frontend-column .form-row.field-xs_order .field-md_ms,.djangocms-frontend-column .form-row.field-xs_order .field-sm_me,.djangocms-frontend-column .form-row.field-xs_order .field-sm_ms,.djangocms-frontend-column .form-row.field-xs_order .field-xl_me,.djangocms-frontend-column .form-row.field-xs_order .field-xl_ms,.djangocms-frontend-column .form-row.field-xs_order .field-xs_me,.djangocms-frontend-column .form-row.field-xs_order .field-xs_ms,.djangocms-frontend-column .form-row.field-xs_order .field-xxl_me,.djangocms-frontend-column .form-row.field-xs_order .field-xxl_ms,.djangocms-frontend-row .form-row.field-row_cols_xs .field-lg_me,.djangocms-frontend-row .form-row.field-row_cols_xs .field-lg_ms,.djangocms-frontend-row .form-row.field-row_cols_xs .field-md_me,.djangocms-frontend-row .form-row.field-row_cols_xs .field-md_ms,.djangocms-frontend-row .form-row.field-row_cols_xs .field-sm_me,.djangocms-frontend-row .form-row.field-row_cols_xs .field-sm_ms,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xl_me,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xl_ms,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xs_me,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xs_ms,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xxl_me,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xxl_ms{text-align:start}.djangocms-frontend-column .form-row.field-xs_col .field-lg_me label,.djangocms-frontend-column .form-row.field-xs_col .field-lg_ms label,.djangocms-frontend-column .form-row.field-xs_col .field-md_me label,.djangocms-frontend-column .form-row.field-xs_col .field-md_ms label,.djangocms-frontend-column .form-row.field-xs_col .field-sm_me label,.djangocms-frontend-column .form-row.field-xs_col .field-sm_ms label,.djangocms-frontend-column .form-row.field-xs_col .field-xl_me label,.djangocms-frontend-column .form-row.field-xs_col .field-xl_ms label,.djangocms-frontend-column .form-row.field-xs_col .field-xs_me label,.djangocms-frontend-column .form-row.field-xs_col .field-xs_ms label,.djangocms-frontend-column .form-row.field-xs_col .field-xxl_me label,.djangocms-frontend-column .form-row.field-xs_col .field-xxl_ms label,.djangocms-frontend-column .form-row.field-xs_me .field-lg_me label,.djangocms-frontend-column .form-row.field-xs_me .field-lg_ms label,.djangocms-frontend-column .form-row.field-xs_me .field-md_me label,.djangocms-frontend-column .form-row.field-xs_me .field-md_ms label,.djangocms-frontend-column .form-row.field-xs_me .field-sm_me label,.djangocms-frontend-column .form-row.field-xs_me .field-sm_ms label,.djangocms-frontend-column .form-row.field-xs_me .field-xl_me label,.djangocms-frontend-column .form-row.field-xs_me .field-xl_ms label,.djangocms-frontend-column .form-row.field-xs_me .field-xs_me label,.djangocms-frontend-column .form-row.field-xs_me .field-xs_ms label,.djangocms-frontend-column .form-row.field-xs_me .field-xxl_me label,.djangocms-frontend-column .form-row.field-xs_me .field-xxl_ms label,.djangocms-frontend-column .form-row.field-xs_ms .field-lg_me label,.djangocms-frontend-column .form-row.field-xs_ms .field-lg_ms label,.djangocms-frontend-column .form-row.field-xs_ms .field-md_me label,.djangocms-frontend-column .form-row.field-xs_ms .field-md_ms label,.djangocms-frontend-column .form-row.field-xs_ms .field-sm_me label,.djangocms-frontend-column .form-row.field-xs_ms .field-sm_ms label,.djangocms-frontend-column .form-row.field-xs_ms .field-xl_me label,.djangocms-frontend-column .form-row.field-xs_ms .field-xl_ms label,.djangocms-frontend-column .form-row.field-xs_ms .field-xs_me label,.djangocms-frontend-column .form-row.field-xs_ms .field-xs_ms label,.djangocms-frontend-column .form-row.field-xs_ms .field-xxl_me label,.djangocms-frontend-column .form-row.field-xs_ms .field-xxl_ms label,.djangocms-frontend-column .form-row.field-xs_offset .field-lg_me label,.djangocms-frontend-column .form-row.field-xs_offset .field-lg_ms label,.djangocms-frontend-column .form-row.field-xs_offset .field-md_me label,.djangocms-frontend-column .form-row.field-xs_offset .field-md_ms label,.djangocms-frontend-column .form-row.field-xs_offset .field-sm_me label,.djangocms-frontend-column .form-row.field-xs_offset .field-sm_ms label,.djangocms-frontend-column .form-row.field-xs_offset .field-xl_me label,.djangocms-frontend-column .form-row.field-xs_offset .field-xl_ms label,.djangocms-frontend-column .form-row.field-xs_offset .field-xs_me label,.djangocms-frontend-column .form-row.field-xs_offset .field-xs_ms label,.djangocms-frontend-column .form-row.field-xs_offset .field-xxl_me label,.djangocms-frontend-column .form-row.field-xs_offset .field-xxl_ms label,.djangocms-frontend-column .form-row.field-xs_order .field-lg_me label,.djangocms-frontend-column .form-row.field-xs_order .field-lg_ms label,.djangocms-frontend-column .form-row.field-xs_order .field-md_me label,.djangocms-frontend-column .form-row.field-xs_order .field-md_ms label,.djangocms-frontend-column .form-row.field-xs_order .field-sm_me label,.djangocms-frontend-column .form-row.field-xs_order .field-sm_ms label,.djangocms-frontend-column .form-row.field-xs_order .field-xl_me label,.djangocms-frontend-column .form-row.field-xs_order .field-xl_ms label,.djangocms-frontend-column .form-row.field-xs_order .field-xs_me label,.djangocms-frontend-column .form-row.field-xs_order .field-xs_ms label,.djangocms-frontend-column .form-row.field-xs_order .field-xxl_me label,.djangocms-frontend-column .form-row.field-xs_order .field-xxl_ms label,.djangocms-frontend-row .form-row.field-row_cols_xs .field-lg_me label,.djangocms-frontend-row .form-row.field-row_cols_xs .field-lg_ms label,.djangocms-frontend-row .form-row.field-row_cols_xs .field-md_me label,.djangocms-frontend-row .form-row.field-row_cols_xs .field-md_ms label,.djangocms-frontend-row .form-row.field-row_cols_xs .field-sm_me label,.djangocms-frontend-row .form-row.field-row_cols_xs .field-sm_ms label,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xl_me label,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xl_ms label,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xs_me label,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xs_ms label,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xxl_me label,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xxl_ms label{inset-inline-start:30px;inset-block-start:14px}.djangocms-frontend-column .form-row.field-xs_col .field-lg_me input,.djangocms-frontend-column .form-row.field-xs_col .field-lg_ms input,.djangocms-frontend-column .form-row.field-xs_col .field-md_me input,.djangocms-frontend-column .form-row.field-xs_col .field-md_ms input,.djangocms-frontend-column .form-row.field-xs_col .field-sm_me input,.djangocms-frontend-column .form-row.field-xs_col .field-sm_ms input,.djangocms-frontend-column .form-row.field-xs_col .field-xl_me input,.djangocms-frontend-column .form-row.field-xs_col .field-xl_ms input,.djangocms-frontend-column .form-row.field-xs_col .field-xs_me input,.djangocms-frontend-column .form-row.field-xs_col .field-xs_ms input,.djangocms-frontend-column .form-row.field-xs_col .field-xxl_me input,.djangocms-frontend-column .form-row.field-xs_col .field-xxl_ms input,.djangocms-frontend-column .form-row.field-xs_me .field-lg_me input,.djangocms-frontend-column .form-row.field-xs_me .field-lg_ms input,.djangocms-frontend-column .form-row.field-xs_me .field-md_me input,.djangocms-frontend-column .form-row.field-xs_me .field-md_ms input,.djangocms-frontend-column .form-row.field-xs_me .field-sm_me input,.djangocms-frontend-column .form-row.field-xs_me .field-sm_ms input,.djangocms-frontend-column .form-row.field-xs_me .field-xl_me input,.djangocms-frontend-column .form-row.field-xs_me .field-xl_ms input,.djangocms-frontend-column .form-row.field-xs_me .field-xs_me input,.djangocms-frontend-column .form-row.field-xs_me .field-xs_ms input,.djangocms-frontend-column .form-row.field-xs_me .field-xxl_me input,.djangocms-frontend-column .form-row.field-xs_me .field-xxl_ms input,.djangocms-frontend-column .form-row.field-xs_ms .field-lg_me input,.djangocms-frontend-column .form-row.field-xs_ms .field-lg_ms input,.djangocms-frontend-column .form-row.field-xs_ms .field-md_me input,.djangocms-frontend-column .form-row.field-xs_ms .field-md_ms input,.djangocms-frontend-column .form-row.field-xs_ms .field-sm_me input,.djangocms-frontend-column .form-row.field-xs_ms .field-sm_ms input,.djangocms-frontend-column .form-row.field-xs_ms .field-xl_me input,.djangocms-frontend-column .form-row.field-xs_ms .field-xl_ms input,.djangocms-frontend-column .form-row.field-xs_ms .field-xs_me input,.djangocms-frontend-column .form-row.field-xs_ms .field-xs_ms input,.djangocms-frontend-column .form-row.field-xs_ms .field-xxl_me input,.djangocms-frontend-column .form-row.field-xs_ms .field-xxl_ms input,.djangocms-frontend-column .form-row.field-xs_offset .field-lg_me input,.djangocms-frontend-column .form-row.field-xs_offset .field-lg_ms input,.djangocms-frontend-column .form-row.field-xs_offset .field-md_me input,.djangocms-frontend-column .form-row.field-xs_offset .field-md_ms input,.djangocms-frontend-column .form-row.field-xs_offset .field-sm_me input,.djangocms-frontend-column .form-row.field-xs_offset .field-sm_ms input,.djangocms-frontend-column .form-row.field-xs_offset .field-xl_me input,.djangocms-frontend-column .form-row.field-xs_offset .field-xl_ms input,.djangocms-frontend-column .form-row.field-xs_offset .field-xs_me input,.djangocms-frontend-column .form-row.field-xs_offset .field-xs_ms input,.djangocms-frontend-column .form-row.field-xs_offset .field-xxl_me input,.djangocms-frontend-column .form-row.field-xs_offset .field-xxl_ms input,.djangocms-frontend-column .form-row.field-xs_order .field-lg_me input,.djangocms-frontend-column .form-row.field-xs_order .field-lg_ms input,.djangocms-frontend-column .form-row.field-xs_order .field-md_me input,.djangocms-frontend-column .form-row.field-xs_order .field-md_ms input,.djangocms-frontend-column .form-row.field-xs_order .field-sm_me input,.djangocms-frontend-column .form-row.field-xs_order .field-sm_ms input,.djangocms-frontend-column .form-row.field-xs_order .field-xl_me input,.djangocms-frontend-column .form-row.field-xs_order .field-xl_ms input,.djangocms-frontend-column .form-row.field-xs_order .field-xs_me input,.djangocms-frontend-column .form-row.field-xs_order .field-xs_ms input,.djangocms-frontend-column .form-row.field-xs_order .field-xxl_me input,.djangocms-frontend-column .form-row.field-xs_order .field-xxl_ms input,.djangocms-frontend-row .form-row.field-row_cols_xs .field-lg_me input,.djangocms-frontend-row .form-row.field-row_cols_xs .field-lg_ms input,.djangocms-frontend-row .form-row.field-row_cols_xs .field-md_me input,.djangocms-frontend-row .form-row.field-row_cols_xs .field-md_ms input,.djangocms-frontend-row .form-row.field-row_cols_xs .field-sm_me input,.djangocms-frontend-row .form-row.field-row_cols_xs .field-sm_ms input,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xl_me input,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xl_ms input,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xs_me input,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xs_ms input,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xxl_me input,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xxl_ms input{position:relative;box-sizing:border-box;top:-3px}.grid-reset{position:absolute;inset-inline-end:5px;inset-block-start:0}.icon-thead{text-align:center;margin-bottom:15px}.icon-thead .icon{font-size:30px}.icon-thead .icon-size-sm{transform:rotate(90deg)}.icon-title{display:block;font-size:12px;color:#999;padding:5px 0 0}.djangocms-frontend-preview{position:fixed;inset-block-start:0;inset-inline-end:0;z-index:10;text-align:center;border-radius:0 0 0 3px;padding:10px 20px 27px;border:1px solid var(--dca-gray,var(--hairline-color,#ccc));border-block-start:none;border-inline-end:none;background:var(--body-bg,#fff)}@media (prefers-color-scheme:dark){.djangocms-frontend-preview{background:var(--body-bg,#000)}}.djangocms-frontend-preview h2{font-size:14px;min-width:150px;margin:0 0 12px}.djangocms-frontend-preview .b4-preview{margin:0 0 -15px}.djangocms-frontend-preview .b4-close{position:absolute;inset-inline-end:10px;inset-block-start:8px;z-index:100;display:block;color:#5e5e5e;font-size:12px;line-height:20px;font-weight:700;text-transform:uppercase;width:20px;height:20px;border-radius:3px;background:#ddd}.djangocms-frontend-preview .b4-close:hover{color:#fff!important;text-decoration:none;background:#0bf}.djangocms-frontend-preview .btn>span{vertical-align:middle}.djangocms-frontend-preview .btn>span>.icon{vertical-align:middle}.djangocms-frontend-preview .btn>span svg,.djangocms-frontend-preview .btn>span use{fill:currentColor}.djangocms-frontend-blockquote textarea{height:110px}#id_link_type{padding:0;margin:0;border:none}#id_link_type li{padding:0;margin:0 15px 5px 0;border:none}#id_link_type label input{position:relative;top:-4px}a[data-pk]{position:relative}a[data-pk]:after{content:attr(data-pk);visibility:hidden;width:auto;font-weight:400;font-size:80%;background-color:var(--dca-white,var(--body-bg,#fff));color:var(--dca-gray,var(--body-fg,#333));border:solid 1px var(--dca-gray,var(--body-fg,#333));text-align:center;padding:5px 10px;position:absolute;z-index:1;top:110%;inset-inline-start:50%;margin-inline-start:-50%}a[data-pk]:hover:after{visibility:visible}.djangocms-admin-style .form-row.field-plugin_title input[name=plugin_title_0]{margin-bottom:.5em!important}.djangocms-admin-style .form-row.field-plugin_title input[name=plugin_title_1]{width:calc(100% - 2em)!important}body:not(.djangocms-admin-style) .form-row.field-plugin_title input[name=plugin_title_1]{width:calc(100% - 200px - 1em)!important;margin-inline-start:1em}.frontend-icon-picker{text-align:center;display:inline-block}.frontend-icon-picker .icon-container{position:relative;margin:.5em auto;width:7em;height:7em;border:1px var(--dca-gray-light,var(--border-color,#d3d3d3)) solid;transition:background-color .15s,color .15s}.frontend-icon-picker .icon-container .icon-preview{width:7em;height:7em;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center}.frontend-icon-picker .icon-container .icon-preview .icon-box{font-size:500%;line-height:1.3;margin:0;text-align:center}.frontend-icon-picker .icon-container .icon-preview .icon-box i,.frontend-icon-picker .icon-container .icon-preview .icon-box span{font-size:unset}.frontend-icon-picker .icon-container .icon-preview .empty-box{text-align:center;overflow:hidden;text-overflow:ellipsis;line-height:1;font-size:100%}.frontend-icon-picker .icon-container .icon-preview .empty-box.hidden{display:none}.frontend-icon-picker .icon-container .icon-preview:hover{background:var(--dca-gray-light,var(--border-color,#d3d3d3));cursor:pointer}.frontend-icon-picker .icon-container .icon-close-indicator{display:block;border-radius:50%;color:var(--dca-black,var(--body-fg,#000));background-color:var(--dca-white,var(--body-bg,#fff));padding:.3rem;border:1px solid var(--dca-black,var(--body-fg,#000));transform:translate(-50%,-50%);top:0;inset-inline-start:100%;width:.6em;height:.6em;line-height:.5em;position:absolute;transition:background-color .15s}.frontend-icon-picker .icon-container .icon-close-indicator:before{content:"×"}.frontend-icon-picker .icon-container .icon-close-indicator:hover{background:var(--delete-button-bg,red);color:var(--delete-button-fg,#fff);cursor:pointer}.uip-modal{position:fixed;height:100%;width:100%;inset-block-end:0;inset-inline-start:0;background-color:rgba(0,0,0,.8);z-index:9999;-webkit-user-select:none;-ms-user-select:none;user-select:none;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.uip-modal *,.uip-modal :after,.uip-modal :before{box-sizing:border-box}.uip-modal.uip-close{opacity:0;visibility:hidden;transition:all .4s ease-in-out}.uip-modal.uip-open{opacity:1;visibility:visible;transition:all .4s ease-in-out}.uip-modal .uip-modal--content{position:absolute;border-radius:3px;box-shadow:2px 8px 23px 3px rgba(0,0,0,.2);overflow:hidden;font-family:Roboto,Arial,Helvetica,Verdana,sans-serif;background-color:var(--dca-gray-lightest,var(--darkened-bg,#f8f8f8));width:100%;margin:auto;left:0;right:0;margin-bottom:2em}.uip-modal .uip-modal--content .uip-modal--header{padding:15px 15px;background-color:var(--dca-white,var(--bg-color,#fff));box-shadow:0 0 8px rgba(0,0,0,.1);position:relative;z-index:1;font-size:15px;color:var(--dca-gray,var(--body-quiet-color,#666));font-weight:500;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.uip-modal .uip-modal--content .uip-modal--header .uip-modal--header-logo-title{padding-top:2px;line-height:1;text-transform:uppercase;font-weight:700;cursor:pointer}.uip-modal .uip-modal--content .uip-modal--header .uip-modal--header-close-btn{cursor:pointer}.uip-modal .uip-modal--content .uip-modal--body{font-size:12px;line-height:1.5;box-sizing:border-box;padding:0;height:70vh;display:-ms-flexbox;display:flex;min-height:50px;max-height:85vh;overflow:auto}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--sidebar{-ms-flex-negative:0;flex-shrink:0;max-width:25%}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--sidebar .uip-modal--sidebar-tabs{margin-top:30px}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--sidebar .uip-modal--sidebar-tabs .uip-modal--sidebar-tab-item{padding:15px;font-size:14px;color:var(--dca-gray,var(--body-quiet-color,#666));text-align:start;cursor:pointer;position:relative;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;text-transform:capitalize}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--sidebar .uip-modal--sidebar-tabs .uip-modal--sidebar-tab-item i{font-size:20px;padding-inline-end:15px;color:var(--dca-gray-lighter,var(--border-color,#ccc))}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--sidebar .uip-modal--sidebar-tabs .uip-modal--sidebar-tab-item img{padding-inline-end:15px}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--sidebar .uip-modal--sidebar-tabs .uip-modal--sidebar-tab-item.universal-active{background-color:var(--dca-white,var(--bg-color,#fff));box-shadow:0 6px 20px 0 rgba(0,0,0,.1)}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--sidebar .uip-modal--sidebar-tabs .uip-modal--sidebar-tab-item.universal-active:after{content:"";position:absolute;height:100%;width:5px;inset-block-start:0;inset-inline-start:0;background-color:#0bf}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--sidebar .uip-modal--sidebar-tabs .uip-modal--sidebar-tab-item.universal-active i{color:#0bf}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--sidebar .uip-modal--sidebar-tabs .uip-modal--sidebar-tab-item:only-child{display:none}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--icon-preview-wrap{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding:30px 80px 0;width:100%}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--icon-preview-wrap .uip-modal--icon-preview-inner{overflow:auto;margin:25px -15px 0;padding:0 15px 15px}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--icon-preview-wrap .uip-modal--icon-preview-inner .uip-modal--icon-preview{display:-ms-grid;display:grid;grid-gap:20px;margin:20px 0}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--icon-preview-wrap .uip-modal--icon-preview-inner .uip-modal--icon-preview .uip-icon-item{position:relative;padding:10px;background-color:var(--dca-white,var(--bg-color,#fff));box-shadow:0 1px 12px rgba(0,0,0,.05);border-radius:3px;cursor:pointer;transition:all .3s;overflow:hidden}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--icon-preview-wrap .uip-modal--icon-preview-inner .uip-modal--icon-preview .uip-icon-item:hover{box-shadow:0 1px 14px rgba(0,0,0,.16)}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--icon-preview-wrap .uip-modal--icon-preview-inner .uip-modal--icon-preview .uip-icon-item.universal-selected{box-shadow:0 1px 12px rgba(0,0,0,.05),0 0 0 3px #0bf}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--icon-preview-wrap .uip-modal--icon-preview-inner .uip-modal--icon-preview .uip-icon-item .uip-icon-item-inner{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-align:center;align-items:center;padding:1px}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--icon-preview-wrap .uip-modal--icon-preview-inner .uip-modal--icon-preview .uip-icon-item .uip-icon-item-inner .uip-icon-item__icon,.uip-modal .uip-modal--content .uip-modal--body .uip-modal--icon-preview-wrap .uip-modal--icon-preview-inner .uip-modal--icon-preview .uip-icon-item .uip-icon-item-inner i{font-size:25px;color:var(--dca-gray-darkest,var(--body-fg,#333))}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--icon-preview-wrap .uip-modal--icon-preview-inner .uip-modal--icon-preview .uip-icon-item .uip-icon-item-inner .uip-icon-item-name{color:var(--dca-gray,var(--body-quiet-color,#666));font-size:11px;padding:13px 0 0;max-width:100%;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;text-transform:capitalize}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--icon-preview-wrap .uip-modal--icon-search{position:relative}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--icon-preview-wrap .uip-modal--icon-search input{width:100%;padding:8px 15px;background-color:var(--dca-white,var(--bg-color,#fff));border:none}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--icon-preview-wrap .uip-modal--icon-search input:-ms-input-placeholder{font-style:italic}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--icon-preview-wrap .uip-modal--icon-search input::placeholder{font-style:italic}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--icon-preview-wrap .uip-modal--icon-search img{position:absolute;top:50%;transform:translateY(-50%);inset-inline-end:10px}.uip-modal .uip-modal--footer{border-top:1px solid var(--dca-gray-lighter,var(--border-color,#ccc));text-align:center;background-color:var(--dca-white,var(--bg-color,#fff));border:none;display:none;-ms-flex-pack:end;justify-content:flex-end;padding:5px;box-shadow:0 0 8px rgba(0,0,0,.1);position:relative;display:-ms-flexbox;display:flex}.uip-modal .uip-modal--footer button.uip-insert-icon-button{padding:10px 35px!important;color:var(--dca-white,var(--bg-color,#fff))!important;background-color:#0bf!important;border:none;cursor:pointer;outline:0}.uip-modal .uip-modal--footer .universal-button{height:40px;margin-inline-start:5px}.uip-modal .uip-modal--footer .universal-button-success{padding:12px 36px;color:var(--dca-white,var(--bg-color,#fff));width:initial}.uip-modal .uip-modal--footer .universal-button-success:hover{background-color:#0bf}@media (min-width:1440px){body:not(.cms-admin-modal) .uip-modal .uip-modal--content{max-width:1200px}}@media (max-width:1439px){body:not(.cms-admin-modal) .uip-modal .uip-modal--content{max-width:990px}.uip-modal--icon-preview-wrap{padding:30px 50px 0}}@media (max-width:1023px){body:not(.cms-admin-modal) .uip-modal .uip-modal--content{max-width:740px}}@media (max-width:767px){.uip-modal--icon-preview-wrap{padding:15px!important}.uip-modal--sidebar{display:none}}@media (min-width:1440px){.uip-modal--icon-preview{-ms-grid-columns:(1fr)[7];grid-template-columns:repeat(7,1fr)}}@media (max-width:1439px){.uip-modal--icon-preview{-ms-grid-columns:(1fr)[6];grid-template-columns:repeat(6,1fr)}}@media (max-width:1024px){.uip-modal--icon-preview{-ms-grid-columns:(1fr)[5];grid-template-columns:repeat(5,1fr)}}@media (max-width:767px){.uip-modal--icon-preview{-ms-grid-columns:(1fr)[4];grid-template-columns:repeat(4,1fr)}}@media (max-width:479px){.uip-modal--icon-preview{-ms-grid-columns:(1fr)[3];grid-template-columns:repeat(3,1fr)}}@media (max-width:1439px){.uip-modal--sidebar-tab-item{padding:15px 15px 15px 25px;font-size:11px}.uip-modal--sidebar-tab-item i{font-size:15px}}@media (max-width:1024px){.uip-modal--sidebar-tab-item i,.uip-modal--sidebar-tab-item img{display:none}}.sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}ul.nav{margin-bottom:1em}ul.nav>li.nav-item{list-style-type:none;padding:inherit}.colM ul:not(.object-tools).nav{margin-top:0;margin-bottom:20px}ul.nav .nav-item{margin-inline-end:1rem}ul.nav .nav-link{position:relative;text-decoration:none}ul.nav .nav-link span.indicator{display:none;border-radius:50%;padding:.5rem;border:1px solid var(--dca-white,var(--body-bg,#fff));transform:translate(-50%,-50%);inset-block-start:0;inset-inline-start:100%;position:absolute}ul.nav .nav-link span.indicator.error{background-color:var(--bs-danger)}ul.nav .nav-link span.indicator.attributes{background-color:var(--bs-info);display:block}ul.nav .nav-link.error>span.indicator{display:block}ul.nav.nav-pills .nav-link:not(.active){border-style:solid;border-width:1px}body:not(.djangocms-admin-style) ul.djangocms-frontend.nav-tabs+div.tab-content .tab-pane{border-left-style:solid;border-bottom-style:solid;border-right-style:solid;border-left-color:var(--hairline-color);border-bottom-color:var(--hairline-color);border-right-color:var(--hairline-color);border-width:1px}body:not(.djangocms-admin-style) ul.djangocms-frontend.nav-tabs+div.tab-content .tab-pane fieldset:last-child{margin-bottom:0}div.tab-pk{-ms-flex-item-align:center;-ms-grid-row-align:center;align-self:center;color:var(--dca-gray-darker,var(--body-fg,#333));font-size:80%;margin-inline-start:auto}.djangocms-admin-style .colM ul.nav:not(.object-tools):not(.messagelist){margin-top:0}.djangocms-admin-style .colM ul.nav:not(.object-tools):not(.messagelist) li.nav-item{border-top:none}input[type=number].auto-field+span{display:none;position:absolute;inset-block-end:0;inset-inline-end:0;text-align:end;margin-inline-end:31px;margin-block-end:23px;cursor:pointer}body:not(.djangocms-admin-style) input[type=number].auto-field+span{margin-bottom:23px}@media (max-width:1024px){body:not(.djangocms-admin-style) input[type=number].auto-field+span{margin-bottom:24px}}input[type=number].auto-field+span:after{content:"auto"}input[type=number].auto-field.auto{color:var(--dca-white,var(--body-bg,#fff));caret-color:var(--dca-black,var(--body-fg,#000))}input[type=number].auto-field.auto+span{display:block} \ No newline at end of file +@charset "UTF-8";:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:33,37,41;--bs-body-bg-rgb:255,255,255;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:0.8125rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-2xl:2rem;--bs-border-radius-pill:50rem;--bs-link-color:#0d6efd;--bs-link-hover-color:#0a58ca;--bs-code-color:#d63384;--bs-highlight-bg:#fff3cd}.btn{--bs-btn-padding-x:0.75rem;--bs-btn-padding-y:0.375rem;--bs-btn-font-size:0.8125rem;--bs-btn-font-weight:400;--bs-btn-line-height:1.5;--bs-btn-color:#212529;--bs-btn-bg:transparent;--bs-btn-border-width:1px;--bs-btn-border-color:transparent;--bs-btn-border-radius:0.375rem;--bs-btn-hover-border-color:transparent;--bs-btn-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.15),0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity:0.65;--bs-btn-focus-box-shadow:0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-ms-user-select:none;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,.btn.active,.btn.show,.btn:first-child:active,:not(.btn-check)+.btn:active{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible,.btn:first-child:active:focus-visible,:not(.btn-check)+.btn:active:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0b5ed7;--bs-btn-hover-border-color:#0a58ca;--bs-btn-focus-shadow-rgb:49,132,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0a58ca;--bs-btn-active-border-color:#0a53be;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#0d6efd;--bs-btn-disabled-border-color:#0d6efd}.btn-secondary{--bs-btn-color:#fff;--bs-btn-bg:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#5c636a;--bs-btn-hover-border-color:#565e64;--bs-btn-focus-shadow-rgb:130,138,145;--bs-btn-active-color:#fff;--bs-btn-active-bg:#565e64;--bs-btn-active-border-color:#51585e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#6c757d;--bs-btn-disabled-border-color:#6c757d}.btn-success{--bs-btn-color:#fff;--bs-btn-bg:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#157347;--bs-btn-hover-border-color:#146c43;--bs-btn-focus-shadow-rgb:60,153,110;--bs-btn-active-color:#fff;--bs-btn-active-bg:#146c43;--bs-btn-active-border-color:#13653f;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#198754;--bs-btn-disabled-border-color:#198754}.btn-info{--bs-btn-color:#000;--bs-btn-bg:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#31d2f2;--bs-btn-hover-border-color:#25cff2;--bs-btn-focus-shadow-rgb:11,172,204;--bs-btn-active-color:#000;--bs-btn-active-bg:#3dd5f3;--bs-btn-active-border-color:#25cff2;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#0dcaf0;--bs-btn-disabled-border-color:#0dcaf0}.btn-warning{--bs-btn-color:#000;--bs-btn-bg:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffca2c;--bs-btn-hover-border-color:#ffc720;--bs-btn-focus-shadow-rgb:217,164,6;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffcd39;--bs-btn-active-border-color:#ffc720;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#ffc107;--bs-btn-disabled-border-color:#ffc107}.btn-danger{--bs-btn-color:#fff;--bs-btn-bg:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#bb2d3b;--bs-btn-hover-border-color:#b02a37;--bs-btn-focus-shadow-rgb:225,83,97;--bs-btn-active-color:#fff;--bs-btn-active-bg:#b02a37;--bs-btn-active-border-color:#a52834;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#dc3545;--bs-btn-disabled-border-color:#dc3545}.btn-light{--bs-btn-color:#000;--bs-btn-bg:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#d3d4d5;--bs-btn-hover-border-color:#c6c7c8;--bs-btn-focus-shadow-rgb:211,212,213;--bs-btn-active-color:#000;--bs-btn-active-bg:#c6c7c8;--bs-btn-active-border-color:#babbbc;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#f8f9fa;--bs-btn-disabled-border-color:#f8f9fa}.btn-dark{--bs-btn-color:#fff;--bs-btn-bg:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#424649;--bs-btn-hover-border-color:#373b3e;--bs-btn-focus-shadow-rgb:66,70,73;--bs-btn-active-color:#fff;--bs-btn-active-bg:#4d5154;--bs-btn-active-border-color:#373b3e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#212529;--bs-btn-disabled-border-color:#212529}.btn-outline-primary{--bs-btn-color:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0d6efd;--bs-btn-hover-border-color:#0d6efd;--bs-btn-focus-shadow-rgb:13,110,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0d6efd;--bs-btn-active-border-color:#0d6efd;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0d6efd;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0d6efd;--bs-gradient:none}.btn-outline-secondary{--bs-btn-color:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6c757d;--bs-btn-hover-border-color:#6c757d;--bs-btn-focus-shadow-rgb:108,117,125;--bs-btn-active-color:#fff;--bs-btn-active-bg:#6c757d;--bs-btn-active-border-color:#6c757d;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#6c757d;--bs-gradient:none}.btn-outline-success{--bs-btn-color:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#198754;--bs-btn-hover-border-color:#198754;--bs-btn-focus-shadow-rgb:25,135,84;--bs-btn-active-color:#fff;--bs-btn-active-bg:#198754;--bs-btn-active-border-color:#198754;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#198754;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#198754;--bs-gradient:none}.btn-outline-info{--bs-btn-color:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#0dcaf0;--bs-btn-hover-border-color:#0dcaf0;--bs-btn-focus-shadow-rgb:13,202,240;--bs-btn-active-color:#000;--bs-btn-active-bg:#0dcaf0;--bs-btn-active-border-color:#0dcaf0;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0dcaf0;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0dcaf0;--bs-gradient:none}.btn-outline-warning{--bs-btn-color:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffc107;--bs-btn-hover-border-color:#ffc107;--bs-btn-focus-shadow-rgb:255,193,7;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffc107;--bs-btn-active-border-color:#ffc107;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#ffc107;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#ffc107;--bs-gradient:none}.btn-outline-danger{--bs-btn-color:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#dc3545;--bs-btn-hover-border-color:#dc3545;--bs-btn-focus-shadow-rgb:220,53,69;--bs-btn-active-color:#fff;--bs-btn-active-bg:#dc3545;--bs-btn-active-border-color:#dc3545;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#dc3545;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#dc3545;--bs-gradient:none}.btn-outline-light{--bs-btn-color:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#f8f9fa;--bs-btn-hover-border-color:#f8f9fa;--bs-btn-focus-shadow-rgb:248,249,250;--bs-btn-active-color:#000;--bs-btn-active-bg:#f8f9fa;--bs-btn-active-border-color:#f8f9fa;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#f8f9fa;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#f8f9fa;--bs-gradient:none}.btn-outline-dark{--bs-btn-color:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#212529;--bs-btn-hover-border-color:#212529;--bs-btn-focus-shadow-rgb:33,37,41;--bs-btn-active-color:#fff;--bs-btn-active-bg:#212529;--bs-btn-active-border-color:#212529;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#212529;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#212529;--bs-gradient:none}.btn-link{--bs-btn-font-weight:400;--bs-btn-color:var(--bs-link-color);--bs-btn-bg:transparent;--bs-btn-border-color:transparent;--bs-btn-hover-color:var(--bs-link-hover-color);--bs-btn-hover-border-color:transparent;--bs-btn-active-color:var(--bs-link-hover-color);--bs-btn-active-border-color:transparent;--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-border-color:transparent;--bs-btn-box-shadow:none;--bs-btn-focus-shadow-rgb:49,132,253;text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-lg{--bs-btn-padding-y:0.5rem;--bs-btn-padding-x:1rem;--bs-btn-font-size:1.015625rem;--bs-btn-border-radius:0.5rem}.btn-sm{--bs-btn-padding-y:0.25rem;--bs-btn-padding-x:0.5rem;--bs-btn-font-size:0.7109375rem;--bs-btn-border-radius:0.25rem}.link-primary{color:#0d6efd!important}.link-primary:focus,.link-primary:hover{color:#0a58ca!important}.link-secondary{color:#6c757d!important}.link-secondary:focus,.link-secondary:hover{color:#565e64!important}.link-success{color:#198754!important}.link-success:focus,.link-success:hover{color:#146c43!important}.link-info{color:#0dcaf0!important}.link-info:focus,.link-info:hover{color:#3dd5f3!important}.link-warning{color:#ffc107!important}.link-warning:focus,.link-warning:hover{color:#ffcd39!important}.link-danger{color:#dc3545!important}.link-danger:focus,.link-danger:hover{color:#b02a37!important}.link-light{color:#f8f9fa!important}.link-light:focus,.link-light:hover{color:#f9fafb!important}.link-dark{color:#212529!important}.link-dark:focus,.link-dark:hover{color:#1a1e21!important}.nav{--bs-nav-link-padding-x:1rem;--bs-nav-link-padding-y:0.3rem;--bs-nav-link-color:var(--bs-link-color);--bs-nav-link-hover-color:var(--bs-link-hover-color);--bs-nav-link-disabled-color:#6c757d;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:var(--bs-nav-link-hover-color)}.nav-link.disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width:1px;--bs-nav-tabs-border-color:#dee2e6;--bs-nav-tabs-border-radius:0.375rem;--bs-nav-tabs-link-hover-border-color:#e9ecef #e9ecef #dee2e6;--bs-nav-tabs-link-active-color:#495057;--bs-nav-tabs-link-active-bg:#fff;--bs-nav-tabs-link-active-border-color:#dee2e6 #dee2e6 #fff;border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--bs-nav-tabs-border-width));background:0 0;border:var(--bs-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-link.disabled,.nav-tabs .nav-link:disabled{color:var(--bs-nav-link-disabled-color);background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1 * var(--bs-nav-tabs-border-width));border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--bs-nav-pills-border-radius:4px;--bs-nav-pills-link-active-color:#fff;--bs-nav-pills-link-active-bg:#0bf}.nav-pills .nav-link{background:0 0;border:0;border-radius:var(--bs-nav-pills-border-radius)}.nav-pills .nav-link:disabled{color:var(--bs-nav-link-disabled-color);background-color:transparent;border-color:transparent}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-fill .nav-item,.nav-fill>.nav-link{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.icon{display:inline-block;vertical-align:top;width:1em;height:1em;background-position:center;background-repeat:no-repeat}.icon svg{display:block;width:100%;height:100%}.icon-info{width:.9em;font-size:110%!important}.icon-white{color:#fff}.icon-white svg{fill:#fff}.icon-black{color:#000}.icon-black svg{fill:#000}.icon-primary{color:#0bf}.icon-primary svg{fill:#0bf}.djangocms-icon .icon>input{float:left;position:relative;top:12px}.djangocms-icon .caret{margin-inline-start:8px}.aligned .frontend-button-group label{min-width:unset}.frontend-button-group{display:inline-block}.frontend-button-group .btn{box-sizing:border-box;cursor:pointer;-webkit-appearance:none;margin:2px;overflow:hidden;text-overflow:ellipsis}.frontend-button-group .btn.active{outline:3px solid #0bf;border-color:#fff!important}.frontend-button-group .btn-default.active{border-radius:0;background-color:#0bf!important}.frontend-button-group-context-colors>div,.frontend-button-group-context-size>div{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;min-height:75px}.frontend-button-group-context-colors .btn{-ms-flex-preferred-size:calc(25% - 4px);flex-basis:calc(25% - 4px)}@media (min-width:820px){.frontend-button-group-context-colors .btn{-ms-flex-preferred-size:calc(20% - 4px);flex-basis:calc(20% - 4px)}}.frontend-button-group-icons .icon,.frontend-grid-icons .icon{font-size:24px}.frontend-button-group-icons .icon-flex-align-center,.frontend-button-group-icons .icon-flex-align-end,.frontend-button-group-icons .icon-flex-align-start,.frontend-grid-icons .icon-flex-align-center,.frontend-grid-icons .icon-flex-align-end,.frontend-grid-icons .icon-flex-align-start{transform:scale(1.4)}.frontend-button-group-icons .icon-flex-content-around,.frontend-button-group-icons .icon-flex-content-between,.frontend-grid-icons .icon-flex-content-around,.frontend-grid-icons .icon-flex-content-between{transform:scale(1.6)}.frontend-button-group-icons .icon-flex-self-center,.frontend-button-group-icons .icon-flex-self-end,.frontend-button-group-icons .icon-flex-self-start,.frontend-grid-icons .icon-flex-self-center,.frontend-grid-icons .icon-flex-self-end,.frontend-grid-icons .icon-flex-self-start{transform:scale(1.4)}.frontend-button-group-icons .icon-align-reset,.frontend-button-group-icons .icon-no-selection,.frontend-grid-icons .icon-align-reset,.frontend-grid-icons .icon-no-selection{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16))}.frontend-button-group-icons .icon-align-items-start,.frontend-button-group-icons .icon-flex-align-start,.frontend-grid-icons .icon-align-items-start,.frontend-grid-icons .icon-flex-align-start{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16));transform:scale(1.4)}.frontend-button-group-icons .icon-align-items-center,.frontend-button-group-icons .icon-flex-align-center,.frontend-grid-icons .icon-align-items-center,.frontend-grid-icons .icon-flex-align-center{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16));transform:scale(1.4)}.frontend-button-group-icons .icon-align-items-end,.frontend-button-group-icons .icon-flex-align-end,.frontend-grid-icons .icon-align-items-end,.frontend-grid-icons .icon-flex-align-end{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16));transform:scale(1.4)}.frontend-button-group-icons .icon-flex-content-start,.frontend-button-group-icons .icon-justify-content-start,.frontend-button-group-icons .icon-start,.frontend-grid-icons .icon-flex-content-start,.frontend-grid-icons .icon-justify-content-start,.frontend-grid-icons .icon-start{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16))}.frontend-button-group-icons .icon-center,.frontend-button-group-icons .icon-flex-content-center,.frontend-button-group-icons .icon-justify-content-center,.frontend-grid-icons .icon-center,.frontend-grid-icons .icon-flex-content-center,.frontend-grid-icons .icon-justify-content-center{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16))}.frontend-button-group-icons .icon-end,.frontend-button-group-icons .icon-flex-content-end,.frontend-button-group-icons .icon-justify-content-end,.frontend-grid-icons .icon-end,.frontend-grid-icons .icon-flex-content-end,.frontend-grid-icons .icon-justify-content-end{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16))}.frontend-button-group-icons .icon-flex-content-around,.frontend-button-group-icons .icon-justify-content-around,.frontend-grid-icons .icon-flex-content-around,.frontend-grid-icons .icon-justify-content-around{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16));transform:scale(1.6)}.frontend-button-group-icons .icon-flex-content-between,.frontend-button-group-icons .icon-justify-content-between,.frontend-grid-icons .icon-flex-content-between,.frontend-grid-icons .icon-justify-content-between{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16));transform:scale(1.6)}.frontend-button-group-icons .icon-nav-fill,.frontend-grid-icons .icon-nav-fill{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16));transform:scale(1.4)}.frontend-button-group-icons .icon-nav-justified,.frontend-grid-icons .icon-nav-justified{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16));transform:scale(1.4)}.frontend-button-group-icons .icon-flex-column,.frontend-grid-icons .icon-flex-column{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16));transform:scale(1.4)}.frontend-button-group-icons .icon-align-self-start,.frontend-button-group-icons .icon-flex-self-start,.frontend-grid-icons .icon-align-self-start,.frontend-grid-icons .icon-flex-self-start{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16));transform:scale(1.4)}.frontend-button-group-icons .icon-align-self-center,.frontend-button-group-icons .icon-flex-self-center,.frontend-grid-icons .icon-align-self-center,.frontend-grid-icons .icon-flex-self-center{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16));transform:scale(1.4)}.frontend-button-group-icons .icon-align-self-end,.frontend-button-group-icons .icon-flex-self-end,.frontend-grid-icons .icon-align-self-end,.frontend-grid-icons .icon-flex-self-end{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16));transform:scale(1.4)}.frontend-button-group-icons .icon-size-sm,.frontend-button-group-icons .icon-size-xs,.frontend-button-group-icons .icon-sm,.frontend-button-group-icons .icon-xs,.frontend-grid-icons .icon-size-sm,.frontend-grid-icons .icon-size-xs,.frontend-grid-icons .icon-sm,.frontend-grid-icons .icon-xs{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16))}.frontend-button-group-icons .icon-size-sm,.frontend-button-group-icons .icon-sm,.frontend-grid-icons .icon-size-sm,.frontend-grid-icons .icon-sm{transform:rotate(-90deg)}.frontend-button-group-icons .icon-md,.frontend-button-group-icons .icon-size-md,.frontend-grid-icons .icon-md,.frontend-grid-icons .icon-size-md{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16))}.frontend-button-group-icons .icon-lg,.frontend-button-group-icons .icon-size-lg,.frontend-grid-icons .icon-lg,.frontend-grid-icons .icon-size-lg{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16))}.frontend-button-group-icons .icon-size-xl,.frontend-button-group-icons .icon-xl,.frontend-grid-icons .icon-size-xl,.frontend-grid-icons .icon-xl{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16))}.frontend-button-group-icons .icon-size-xxl,.frontend-button-group-icons .icon-xxl,.frontend-grid-icons .icon-size-xxl,.frontend-grid-icons .icon-xxl{background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 16))}.frontend-button-group-icons .icon-mb,.frontend-grid-icons .icon-mb{transform:scale(1.3);background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 100))}.frontend-button-group-icons .icon-me,.frontend-grid-icons .icon-me{transform:scale(1.3);background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 100))}.frontend-button-group-icons .icon-ms,.frontend-grid-icons .icon-ms{transform:scale(1.3);background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 100))}.frontend-button-group-icons .icon-mt,.frontend-grid-icons .icon-mt{transform:scale(1.3);background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 100))}.frontend-button-group-icons .icon-mx,.frontend-grid-icons .icon-mx{transform:scale(1.3);background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 100))}.frontend-button-group-icons .icon-my,.frontend-grid-icons .icon-my{transform:scale(1.3);background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 100))}.frontend-button-group-icons .icon-pb,.frontend-grid-icons .icon-pb{transform:scale(1.5);background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 100))}.frontend-button-group-icons .icon-pe,.frontend-grid-icons .icon-pe{transform:scale(1.5);background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 100))}.frontend-button-group-icons .icon-ps,.frontend-grid-icons .icon-ps{transform:scale(1.5);background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 100))}.frontend-button-group-icons .icon-pt,.frontend-grid-icons .icon-pt{transform:scale(1.5);background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 100))}.frontend-button-group-icons .icon-px,.frontend-grid-icons .icon-px{transform:scale(1.5);background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 100))}.frontend-button-group-icons .icon-py,.frontend-grid-icons .icon-py{transform:scale(1.5);background-image:url('data:image/svg+xml;utf8,');filter:brightness(calc(var(--dca-light-mode, 1) + var(--dca-dark-mode, 0) * 100))}.icon-info{background-image:url('data:image/svg+xml;utf8,')}.module{margin:0 0 20px}.djangocms-frontend-row .form-row.field-create .icon{position:absolute;font-size:30px;margin-block-start:28px;margin-inline-start:4px}.djangocms-frontend-row .form-row.field-create input[name=create]{width:130px!important;padding-inline-end:5px!important;text-align:start}.djangocms-frontend-column .form-row.field-xs_col,.djangocms-frontend-column .form-row.field-xs_me,.djangocms-frontend-column .form-row.field-xs_ms,.djangocms-frontend-column .form-row.field-xs_offset,.djangocms-frontend-column .form-row.field-xs_order,.djangocms-frontend-row .form-row.field-row_cols_xs{position:relative;display:-ms-flexbox;display:flex;padding:0;min-width:800px}.djangocms-frontend-column .form-row.field-xs_col>div>div:not([class]),.djangocms-frontend-column .form-row.field-xs_me>div>div:not([class]),.djangocms-frontend-column .form-row.field-xs_ms>div>div:not([class]),.djangocms-frontend-column .form-row.field-xs_offset>div>div:not([class]),.djangocms-frontend-column .form-row.field-xs_order>div>div:not([class]),.djangocms-frontend-row .form-row.field-row_cols_xs>div>div:not([class]){width:unset!important;max-width:unset!important}.djangocms-frontend-column .form-row.field-xs_col .field-box:first-child,.djangocms-frontend-column .form-row.field-xs_me .field-box:first-child,.djangocms-frontend-column .form-row.field-xs_ms .field-box:first-child,.djangocms-frontend-column .form-row.field-xs_offset .field-box:first-child,.djangocms-frontend-column .form-row.field-xs_order .field-box:first-child,.djangocms-frontend-row .form-row.field-row_cols_xs .field-box:first-child{width:115px!important}.djangocms-frontend-column .form-row.field-xs_col .field-box,.djangocms-frontend-column .form-row.field-xs_col .fieldBox,.djangocms-frontend-column .form-row.field-xs_me .field-box,.djangocms-frontend-column .form-row.field-xs_me .fieldBox,.djangocms-frontend-column .form-row.field-xs_ms .field-box,.djangocms-frontend-column .form-row.field-xs_ms .fieldBox,.djangocms-frontend-column .form-row.field-xs_offset .field-box,.djangocms-frontend-column .form-row.field-xs_offset .fieldBox,.djangocms-frontend-column .form-row.field-xs_order .field-box,.djangocms-frontend-column .form-row.field-xs_order .fieldBox,.djangocms-frontend-row .form-row.field-row_cols_xs .field-box,.djangocms-frontend-row .form-row.field-row_cols_xs .fieldBox{position:relative;box-sizing:content-box;width:86px!important;-ms-flex:none;flex:none;display:block;padding:15px 10px;margin:0!important;border-bottom:1px solid #eee;float:left!important}.djangocms-frontend-column .form-row.field-xs_col .field-box input:not([type=checkbox]),.djangocms-frontend-column .form-row.field-xs_col .fieldBox input:not([type=checkbox]),.djangocms-frontend-column .form-row.field-xs_me .field-box input:not([type=checkbox]),.djangocms-frontend-column .form-row.field-xs_me .fieldBox input:not([type=checkbox]),.djangocms-frontend-column .form-row.field-xs_ms .field-box input:not([type=checkbox]),.djangocms-frontend-column .form-row.field-xs_ms .fieldBox input:not([type=checkbox]),.djangocms-frontend-column .form-row.field-xs_offset .field-box input:not([type=checkbox]),.djangocms-frontend-column .form-row.field-xs_offset .fieldBox input:not([type=checkbox]),.djangocms-frontend-column .form-row.field-xs_order .field-box input:not([type=checkbox]),.djangocms-frontend-column .form-row.field-xs_order .fieldBox input:not([type=checkbox]),.djangocms-frontend-row .form-row.field-row_cols_xs .field-box input:not([type=checkbox]),.djangocms-frontend-row .form-row.field-row_cols_xs .fieldBox input:not([type=checkbox]){text-align:end;padding-inline-end:5px!important;box-sizing:border-box;width:100%}.djangocms-frontend-column .form-row.field-xs_col .field-box label,.djangocms-frontend-column .form-row.field-xs_col .fieldBox label,.djangocms-frontend-column .form-row.field-xs_me .field-box label,.djangocms-frontend-column .form-row.field-xs_me .fieldBox label,.djangocms-frontend-column .form-row.field-xs_ms .field-box label,.djangocms-frontend-column .form-row.field-xs_ms .fieldBox label,.djangocms-frontend-column .form-row.field-xs_offset .field-box label,.djangocms-frontend-column .form-row.field-xs_offset .fieldBox label,.djangocms-frontend-column .form-row.field-xs_order .field-box label,.djangocms-frontend-column .form-row.field-xs_order .fieldBox label,.djangocms-frontend-row .form-row.field-row_cols_xs .field-box label,.djangocms-frontend-row .form-row.field-row_cols_xs .fieldBox label{font-size:12px!important;font-weight:400!important;color:#ccc!important;position:absolute;inset-inline-start:15px;inset-block-end:17px;text-transform:lowercase}.djangocms-frontend-column .form-row.field-xs_col .field-box .disabled,.djangocms-frontend-column .form-row.field-xs_col .fieldBox .disabled,.djangocms-frontend-column .form-row.field-xs_me .field-box .disabled,.djangocms-frontend-column .form-row.field-xs_me .fieldBox .disabled,.djangocms-frontend-column .form-row.field-xs_ms .field-box .disabled,.djangocms-frontend-column .form-row.field-xs_ms .fieldBox .disabled,.djangocms-frontend-column .form-row.field-xs_offset .field-box .disabled,.djangocms-frontend-column .form-row.field-xs_offset .fieldBox .disabled,.djangocms-frontend-column .form-row.field-xs_order .field-box .disabled,.djangocms-frontend-column .form-row.field-xs_order .fieldBox .disabled,.djangocms-frontend-row .form-row.field-row_cols_xs .field-box .disabled,.djangocms-frontend-row .form-row.field-row_cols_xs .fieldBox .disabled{color:#ccc;background:#eee}.djangocms-frontend-column .form-row.field-xs_col .field-box:last-child,.djangocms-frontend-column .form-row.field-xs_col .fieldBox:last-child,.djangocms-frontend-column .form-row.field-xs_me .field-box:last-child,.djangocms-frontend-column .form-row.field-xs_me .fieldBox:last-child,.djangocms-frontend-column .form-row.field-xs_ms .field-box:last-child,.djangocms-frontend-column .form-row.field-xs_ms .fieldBox:last-child,.djangocms-frontend-column .form-row.field-xs_offset .field-box:last-child,.djangocms-frontend-column .form-row.field-xs_offset .fieldBox:last-child,.djangocms-frontend-column .form-row.field-xs_order .field-box:last-child,.djangocms-frontend-column .form-row.field-xs_order .fieldBox:last-child,.djangocms-frontend-row .form-row.field-row_cols_xs .field-box:last-child,.djangocms-frontend-row .form-row.field-row_cols_xs .fieldBox:last-child{border-inline-end:none}.djangocms-frontend-column .form-row.field-xs_col .errors,.djangocms-frontend-column .form-row.field-xs_me .errors,.djangocms-frontend-column .form-row.field-xs_ms .errors,.djangocms-frontend-column .form-row.field-xs_offset .errors,.djangocms-frontend-column .form-row.field-xs_order .errors,.djangocms-frontend-row .form-row.field-row_cols_xs .errors{margin-bottom:0}.djangocms-frontend-column .form-row.field-xs_col .errorlist,.djangocms-frontend-column .form-row.field-xs_me .errorlist,.djangocms-frontend-column .form-row.field-xs_ms .errorlist,.djangocms-frontend-column .form-row.field-xs_offset .errorlist,.djangocms-frontend-column .form-row.field-xs_order .errorlist,.djangocms-frontend-row .form-row.field-row_cols_xs .errorlist{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.djangocms-frontend-column .form-row.field-xs_col.field-xs_me .field-box,.djangocms-frontend-column .form-row.field-xs_me.field-xs_me .field-box,.djangocms-frontend-column .form-row.field-xs_ms.field-xs_me .field-box,.djangocms-frontend-column .form-row.field-xs_offset.field-xs_me .field-box,.djangocms-frontend-column .form-row.field-xs_order.field-xs_me .field-box,.djangocms-frontend-row .form-row.field-row_cols_xs.field-xs_me .field-box{border-bottom:none}.djangocms-frontend-column .form-row.field-xs_col .field-box-label,.djangocms-frontend-column .form-row.field-xs_me .field-box-label,.djangocms-frontend-column .form-row.field-xs_ms .field-box-label,.djangocms-frontend-column .form-row.field-xs_offset .field-box-label,.djangocms-frontend-column .form-row.field-xs_order .field-box-label,.djangocms-frontend-row .form-row.field-row_cols_xs .field-box-label{display:-ms-flexbox;display:flex;margin-top:auto;color:#999}.djangocms-frontend-column .form-row.field-xs_col .field-box-label a,.djangocms-frontend-column .form-row.field-xs_me .field-box-label a,.djangocms-frontend-column .form-row.field-xs_ms .field-box-label a,.djangocms-frontend-column .form-row.field-xs_offset .field-box-label a,.djangocms-frontend-column .form-row.field-xs_order .field-box-label a,.djangocms-frontend-row .form-row.field-row_cols_xs .field-box-label a{width:100%;margin-top:auto;color:#999}.djangocms-frontend-column .form-row.field-xs_col .field-box-label a a,.djangocms-frontend-column .form-row.field-xs_me .field-box-label a a,.djangocms-frontend-column .form-row.field-xs_ms .field-box-label a a,.djangocms-frontend-column .form-row.field-xs_offset .field-box-label a a,.djangocms-frontend-column .form-row.field-xs_order .field-box-label a a,.djangocms-frontend-row .form-row.field-row_cols_xs .field-box-label a a{width:100%;margin-top:auto}.djangocms-frontend-column .form-row.field-xs_col .field-lg_me,.djangocms-frontend-column .form-row.field-xs_col .field-lg_ms,.djangocms-frontend-column .form-row.field-xs_col .field-md_me,.djangocms-frontend-column .form-row.field-xs_col .field-md_ms,.djangocms-frontend-column .form-row.field-xs_col .field-sm_me,.djangocms-frontend-column .form-row.field-xs_col .field-sm_ms,.djangocms-frontend-column .form-row.field-xs_col .field-xl_me,.djangocms-frontend-column .form-row.field-xs_col .field-xl_ms,.djangocms-frontend-column .form-row.field-xs_col .field-xs_me,.djangocms-frontend-column .form-row.field-xs_col .field-xs_ms,.djangocms-frontend-column .form-row.field-xs_col .field-xxl_me,.djangocms-frontend-column .form-row.field-xs_col .field-xxl_ms,.djangocms-frontend-column .form-row.field-xs_me .field-lg_me,.djangocms-frontend-column .form-row.field-xs_me .field-lg_ms,.djangocms-frontend-column .form-row.field-xs_me .field-md_me,.djangocms-frontend-column .form-row.field-xs_me .field-md_ms,.djangocms-frontend-column .form-row.field-xs_me .field-sm_me,.djangocms-frontend-column .form-row.field-xs_me .field-sm_ms,.djangocms-frontend-column .form-row.field-xs_me .field-xl_me,.djangocms-frontend-column .form-row.field-xs_me .field-xl_ms,.djangocms-frontend-column .form-row.field-xs_me .field-xs_me,.djangocms-frontend-column .form-row.field-xs_me .field-xs_ms,.djangocms-frontend-column .form-row.field-xs_me .field-xxl_me,.djangocms-frontend-column .form-row.field-xs_me .field-xxl_ms,.djangocms-frontend-column .form-row.field-xs_ms .field-lg_me,.djangocms-frontend-column .form-row.field-xs_ms .field-lg_ms,.djangocms-frontend-column .form-row.field-xs_ms .field-md_me,.djangocms-frontend-column .form-row.field-xs_ms .field-md_ms,.djangocms-frontend-column .form-row.field-xs_ms .field-sm_me,.djangocms-frontend-column .form-row.field-xs_ms .field-sm_ms,.djangocms-frontend-column .form-row.field-xs_ms .field-xl_me,.djangocms-frontend-column .form-row.field-xs_ms .field-xl_ms,.djangocms-frontend-column .form-row.field-xs_ms .field-xs_me,.djangocms-frontend-column .form-row.field-xs_ms .field-xs_ms,.djangocms-frontend-column .form-row.field-xs_ms .field-xxl_me,.djangocms-frontend-column .form-row.field-xs_ms .field-xxl_ms,.djangocms-frontend-column .form-row.field-xs_offset .field-lg_me,.djangocms-frontend-column .form-row.field-xs_offset .field-lg_ms,.djangocms-frontend-column .form-row.field-xs_offset .field-md_me,.djangocms-frontend-column .form-row.field-xs_offset .field-md_ms,.djangocms-frontend-column .form-row.field-xs_offset .field-sm_me,.djangocms-frontend-column .form-row.field-xs_offset .field-sm_ms,.djangocms-frontend-column .form-row.field-xs_offset .field-xl_me,.djangocms-frontend-column .form-row.field-xs_offset .field-xl_ms,.djangocms-frontend-column .form-row.field-xs_offset .field-xs_me,.djangocms-frontend-column .form-row.field-xs_offset .field-xs_ms,.djangocms-frontend-column .form-row.field-xs_offset .field-xxl_me,.djangocms-frontend-column .form-row.field-xs_offset .field-xxl_ms,.djangocms-frontend-column .form-row.field-xs_order .field-lg_me,.djangocms-frontend-column .form-row.field-xs_order .field-lg_ms,.djangocms-frontend-column .form-row.field-xs_order .field-md_me,.djangocms-frontend-column .form-row.field-xs_order .field-md_ms,.djangocms-frontend-column .form-row.field-xs_order .field-sm_me,.djangocms-frontend-column .form-row.field-xs_order .field-sm_ms,.djangocms-frontend-column .form-row.field-xs_order .field-xl_me,.djangocms-frontend-column .form-row.field-xs_order .field-xl_ms,.djangocms-frontend-column .form-row.field-xs_order .field-xs_me,.djangocms-frontend-column .form-row.field-xs_order .field-xs_ms,.djangocms-frontend-column .form-row.field-xs_order .field-xxl_me,.djangocms-frontend-column .form-row.field-xs_order .field-xxl_ms,.djangocms-frontend-row .form-row.field-row_cols_xs .field-lg_me,.djangocms-frontend-row .form-row.field-row_cols_xs .field-lg_ms,.djangocms-frontend-row .form-row.field-row_cols_xs .field-md_me,.djangocms-frontend-row .form-row.field-row_cols_xs .field-md_ms,.djangocms-frontend-row .form-row.field-row_cols_xs .field-sm_me,.djangocms-frontend-row .form-row.field-row_cols_xs .field-sm_ms,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xl_me,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xl_ms,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xs_me,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xs_ms,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xxl_me,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xxl_ms{text-align:start}.djangocms-frontend-column .form-row.field-xs_col .field-lg_me label,.djangocms-frontend-column .form-row.field-xs_col .field-lg_ms label,.djangocms-frontend-column .form-row.field-xs_col .field-md_me label,.djangocms-frontend-column .form-row.field-xs_col .field-md_ms label,.djangocms-frontend-column .form-row.field-xs_col .field-sm_me label,.djangocms-frontend-column .form-row.field-xs_col .field-sm_ms label,.djangocms-frontend-column .form-row.field-xs_col .field-xl_me label,.djangocms-frontend-column .form-row.field-xs_col .field-xl_ms label,.djangocms-frontend-column .form-row.field-xs_col .field-xs_me label,.djangocms-frontend-column .form-row.field-xs_col .field-xs_ms label,.djangocms-frontend-column .form-row.field-xs_col .field-xxl_me label,.djangocms-frontend-column .form-row.field-xs_col .field-xxl_ms label,.djangocms-frontend-column .form-row.field-xs_me .field-lg_me label,.djangocms-frontend-column .form-row.field-xs_me .field-lg_ms label,.djangocms-frontend-column .form-row.field-xs_me .field-md_me label,.djangocms-frontend-column .form-row.field-xs_me .field-md_ms label,.djangocms-frontend-column .form-row.field-xs_me .field-sm_me label,.djangocms-frontend-column .form-row.field-xs_me .field-sm_ms label,.djangocms-frontend-column .form-row.field-xs_me .field-xl_me label,.djangocms-frontend-column .form-row.field-xs_me .field-xl_ms label,.djangocms-frontend-column .form-row.field-xs_me .field-xs_me label,.djangocms-frontend-column .form-row.field-xs_me .field-xs_ms label,.djangocms-frontend-column .form-row.field-xs_me .field-xxl_me label,.djangocms-frontend-column .form-row.field-xs_me .field-xxl_ms label,.djangocms-frontend-column .form-row.field-xs_ms .field-lg_me label,.djangocms-frontend-column .form-row.field-xs_ms .field-lg_ms label,.djangocms-frontend-column .form-row.field-xs_ms .field-md_me label,.djangocms-frontend-column .form-row.field-xs_ms .field-md_ms label,.djangocms-frontend-column .form-row.field-xs_ms .field-sm_me label,.djangocms-frontend-column .form-row.field-xs_ms .field-sm_ms label,.djangocms-frontend-column .form-row.field-xs_ms .field-xl_me label,.djangocms-frontend-column .form-row.field-xs_ms .field-xl_ms label,.djangocms-frontend-column .form-row.field-xs_ms .field-xs_me label,.djangocms-frontend-column .form-row.field-xs_ms .field-xs_ms label,.djangocms-frontend-column .form-row.field-xs_ms .field-xxl_me label,.djangocms-frontend-column .form-row.field-xs_ms .field-xxl_ms label,.djangocms-frontend-column .form-row.field-xs_offset .field-lg_me label,.djangocms-frontend-column .form-row.field-xs_offset .field-lg_ms label,.djangocms-frontend-column .form-row.field-xs_offset .field-md_me label,.djangocms-frontend-column .form-row.field-xs_offset .field-md_ms label,.djangocms-frontend-column .form-row.field-xs_offset .field-sm_me label,.djangocms-frontend-column .form-row.field-xs_offset .field-sm_ms label,.djangocms-frontend-column .form-row.field-xs_offset .field-xl_me label,.djangocms-frontend-column .form-row.field-xs_offset .field-xl_ms label,.djangocms-frontend-column .form-row.field-xs_offset .field-xs_me label,.djangocms-frontend-column .form-row.field-xs_offset .field-xs_ms label,.djangocms-frontend-column .form-row.field-xs_offset .field-xxl_me label,.djangocms-frontend-column .form-row.field-xs_offset .field-xxl_ms label,.djangocms-frontend-column .form-row.field-xs_order .field-lg_me label,.djangocms-frontend-column .form-row.field-xs_order .field-lg_ms label,.djangocms-frontend-column .form-row.field-xs_order .field-md_me label,.djangocms-frontend-column .form-row.field-xs_order .field-md_ms label,.djangocms-frontend-column .form-row.field-xs_order .field-sm_me label,.djangocms-frontend-column .form-row.field-xs_order .field-sm_ms label,.djangocms-frontend-column .form-row.field-xs_order .field-xl_me label,.djangocms-frontend-column .form-row.field-xs_order .field-xl_ms label,.djangocms-frontend-column .form-row.field-xs_order .field-xs_me label,.djangocms-frontend-column .form-row.field-xs_order .field-xs_ms label,.djangocms-frontend-column .form-row.field-xs_order .field-xxl_me label,.djangocms-frontend-column .form-row.field-xs_order .field-xxl_ms label,.djangocms-frontend-row .form-row.field-row_cols_xs .field-lg_me label,.djangocms-frontend-row .form-row.field-row_cols_xs .field-lg_ms label,.djangocms-frontend-row .form-row.field-row_cols_xs .field-md_me label,.djangocms-frontend-row .form-row.field-row_cols_xs .field-md_ms label,.djangocms-frontend-row .form-row.field-row_cols_xs .field-sm_me label,.djangocms-frontend-row .form-row.field-row_cols_xs .field-sm_ms label,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xl_me label,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xl_ms label,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xs_me label,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xs_ms label,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xxl_me label,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xxl_ms label{inset-inline-start:30px;inset-block-start:14px}.djangocms-frontend-column .form-row.field-xs_col .field-lg_me input,.djangocms-frontend-column .form-row.field-xs_col .field-lg_ms input,.djangocms-frontend-column .form-row.field-xs_col .field-md_me input,.djangocms-frontend-column .form-row.field-xs_col .field-md_ms input,.djangocms-frontend-column .form-row.field-xs_col .field-sm_me input,.djangocms-frontend-column .form-row.field-xs_col .field-sm_ms input,.djangocms-frontend-column .form-row.field-xs_col .field-xl_me input,.djangocms-frontend-column .form-row.field-xs_col .field-xl_ms input,.djangocms-frontend-column .form-row.field-xs_col .field-xs_me input,.djangocms-frontend-column .form-row.field-xs_col .field-xs_ms input,.djangocms-frontend-column .form-row.field-xs_col .field-xxl_me input,.djangocms-frontend-column .form-row.field-xs_col .field-xxl_ms input,.djangocms-frontend-column .form-row.field-xs_me .field-lg_me input,.djangocms-frontend-column .form-row.field-xs_me .field-lg_ms input,.djangocms-frontend-column .form-row.field-xs_me .field-md_me input,.djangocms-frontend-column .form-row.field-xs_me .field-md_ms input,.djangocms-frontend-column .form-row.field-xs_me .field-sm_me input,.djangocms-frontend-column .form-row.field-xs_me .field-sm_ms input,.djangocms-frontend-column .form-row.field-xs_me .field-xl_me input,.djangocms-frontend-column .form-row.field-xs_me .field-xl_ms input,.djangocms-frontend-column .form-row.field-xs_me .field-xs_me input,.djangocms-frontend-column .form-row.field-xs_me .field-xs_ms input,.djangocms-frontend-column .form-row.field-xs_me .field-xxl_me input,.djangocms-frontend-column .form-row.field-xs_me .field-xxl_ms input,.djangocms-frontend-column .form-row.field-xs_ms .field-lg_me input,.djangocms-frontend-column .form-row.field-xs_ms .field-lg_ms input,.djangocms-frontend-column .form-row.field-xs_ms .field-md_me input,.djangocms-frontend-column .form-row.field-xs_ms .field-md_ms input,.djangocms-frontend-column .form-row.field-xs_ms .field-sm_me input,.djangocms-frontend-column .form-row.field-xs_ms .field-sm_ms input,.djangocms-frontend-column .form-row.field-xs_ms .field-xl_me input,.djangocms-frontend-column .form-row.field-xs_ms .field-xl_ms input,.djangocms-frontend-column .form-row.field-xs_ms .field-xs_me input,.djangocms-frontend-column .form-row.field-xs_ms .field-xs_ms input,.djangocms-frontend-column .form-row.field-xs_ms .field-xxl_me input,.djangocms-frontend-column .form-row.field-xs_ms .field-xxl_ms input,.djangocms-frontend-column .form-row.field-xs_offset .field-lg_me input,.djangocms-frontend-column .form-row.field-xs_offset .field-lg_ms input,.djangocms-frontend-column .form-row.field-xs_offset .field-md_me input,.djangocms-frontend-column .form-row.field-xs_offset .field-md_ms input,.djangocms-frontend-column .form-row.field-xs_offset .field-sm_me input,.djangocms-frontend-column .form-row.field-xs_offset .field-sm_ms input,.djangocms-frontend-column .form-row.field-xs_offset .field-xl_me input,.djangocms-frontend-column .form-row.field-xs_offset .field-xl_ms input,.djangocms-frontend-column .form-row.field-xs_offset .field-xs_me input,.djangocms-frontend-column .form-row.field-xs_offset .field-xs_ms input,.djangocms-frontend-column .form-row.field-xs_offset .field-xxl_me input,.djangocms-frontend-column .form-row.field-xs_offset .field-xxl_ms input,.djangocms-frontend-column .form-row.field-xs_order .field-lg_me input,.djangocms-frontend-column .form-row.field-xs_order .field-lg_ms input,.djangocms-frontend-column .form-row.field-xs_order .field-md_me input,.djangocms-frontend-column .form-row.field-xs_order .field-md_ms input,.djangocms-frontend-column .form-row.field-xs_order .field-sm_me input,.djangocms-frontend-column .form-row.field-xs_order .field-sm_ms input,.djangocms-frontend-column .form-row.field-xs_order .field-xl_me input,.djangocms-frontend-column .form-row.field-xs_order .field-xl_ms input,.djangocms-frontend-column .form-row.field-xs_order .field-xs_me input,.djangocms-frontend-column .form-row.field-xs_order .field-xs_ms input,.djangocms-frontend-column .form-row.field-xs_order .field-xxl_me input,.djangocms-frontend-column .form-row.field-xs_order .field-xxl_ms input,.djangocms-frontend-row .form-row.field-row_cols_xs .field-lg_me input,.djangocms-frontend-row .form-row.field-row_cols_xs .field-lg_ms input,.djangocms-frontend-row .form-row.field-row_cols_xs .field-md_me input,.djangocms-frontend-row .form-row.field-row_cols_xs .field-md_ms input,.djangocms-frontend-row .form-row.field-row_cols_xs .field-sm_me input,.djangocms-frontend-row .form-row.field-row_cols_xs .field-sm_ms input,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xl_me input,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xl_ms input,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xs_me input,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xs_ms input,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xxl_me input,.djangocms-frontend-row .form-row.field-row_cols_xs .field-xxl_ms input{position:relative;box-sizing:border-box;top:-3px}.grid-reset{position:absolute;inset-inline-end:5px;inset-block-start:0}.icon-thead{text-align:center;margin-bottom:15px}.icon-thead .icon{font-size:30px}.icon-thead .icon-size-sm{transform:rotate(90deg)}.icon-title{display:block;font-size:12px;color:#999;padding:5px 0 0}.djangocms-frontend-preview{position:fixed;inset-block-start:0;inset-inline-end:0;z-index:10;text-align:center;border-radius:0 0 0 3px;padding:10px 20px 27px;border:1px solid var(--dca-gray,var(--hairline-color,#ccc));border-block-start:none;border-inline-end:none;background:var(--body-bg,#fff)}@media (prefers-color-scheme:dark){.djangocms-frontend-preview{background:var(--body-bg,#000)}}.djangocms-frontend-preview h2{font-size:14px;min-width:150px;margin:0 0 12px}.djangocms-frontend-preview .b4-preview{margin:0 0 -15px}.djangocms-frontend-preview .b4-close{position:absolute;inset-inline-end:10px;inset-block-start:8px;z-index:100;display:block;color:#5e5e5e;font-size:12px;line-height:20px;font-weight:700;text-transform:uppercase;width:20px;height:20px;border-radius:3px;background:#ddd}.djangocms-frontend-preview .b4-close:hover{color:#fff!important;text-decoration:none;background:#0bf}.djangocms-frontend-preview .btn>span{vertical-align:middle}.djangocms-frontend-preview .btn>span>.icon{vertical-align:middle}.djangocms-frontend-preview .btn>span svg,.djangocms-frontend-preview .btn>span use{fill:currentColor}.djangocms-frontend-blockquote textarea{height:110px}#id_link_type{padding:0;margin:0;border:none}#id_link_type li{padding:0;margin:0 15px 5px 0;border:none}#id_link_type label input{position:relative;top:-4px}a[data-pk]{position:relative}a[data-pk]:after{content:attr(data-pk);visibility:hidden;width:auto;font-weight:400;font-size:80%;background-color:var(--dca-white,var(--body-bg,#fff));color:var(--dca-gray,var(--body-fg,#333));border:solid 1px var(--dca-gray,var(--body-fg,#333));text-align:center;padding:5px 10px;position:absolute;z-index:1;top:110%;inset-inline-start:50%;margin-inline-start:-50%}a[data-pk]:hover:after{visibility:visible}.djangocms-admin-style .form-row.field-plugin_title input[name=plugin_title_0]{margin-bottom:.5em!important}.djangocms-admin-style .form-row.field-plugin_title input[name=plugin_title_1]{width:calc(100% - 2em)!important}body:not(.djangocms-admin-style) .form-row.field-plugin_title input[name=plugin_title_1]{width:calc(100% - 200px - 1em)!important;margin-inline-start:1em}.frontend-icon-picker{text-align:center;display:inline-block}.frontend-icon-picker .icon-container{position:relative;margin:.5em auto;width:7em;height:7em;border:1px var(--dca-gray-light,var(--border-color,#d3d3d3)) solid;transition:background-color .15s,color .15s}.frontend-icon-picker .icon-container .icon-preview{width:7em;height:7em;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center}.frontend-icon-picker .icon-container .icon-preview .icon-box{font-size:500%;line-height:1.3;margin:0;text-align:center}.frontend-icon-picker .icon-container .icon-preview .icon-box i,.frontend-icon-picker .icon-container .icon-preview .icon-box span{font-size:unset}.frontend-icon-picker .icon-container .icon-preview .empty-box{text-align:center;overflow:hidden;text-overflow:ellipsis;line-height:1;font-size:100%}.frontend-icon-picker .icon-container .icon-preview .empty-box.hidden{display:none}.frontend-icon-picker .icon-container .icon-preview:hover{background:var(--dca-gray-light,var(--border-color,#d3d3d3));cursor:pointer}.frontend-icon-picker .icon-container .icon-close-indicator{display:block;border-radius:50%;color:var(--dca-black,var(--body-fg,#000));background-color:var(--dca-white,var(--body-bg,#fff));padding:.3rem;border:1px solid var(--dca-black,var(--body-fg,#000));transform:translate(-50%,-50%);top:0;inset-inline-start:100%;width:.6em;height:.6em;line-height:.5em;position:absolute;transition:background-color .15s}.frontend-icon-picker .icon-container .icon-close-indicator:before{content:"×"}.frontend-icon-picker .icon-container .icon-close-indicator:hover{background:var(--delete-button-bg,red);color:var(--delete-button-fg,#fff);cursor:pointer}.uip-modal{position:fixed;height:100%;width:100%;inset-block-end:0;inset-inline-start:0;background-color:rgba(0,0,0,.8);z-index:9999;-webkit-user-select:none;-ms-user-select:none;user-select:none;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.uip-modal *,.uip-modal :after,.uip-modal :before{box-sizing:border-box}.uip-modal.uip-close{opacity:0;visibility:hidden;transition:all .4s ease-in-out}.uip-modal.uip-open{opacity:1;visibility:visible;transition:all .4s ease-in-out}.uip-modal .uip-modal--content{position:absolute;border-radius:3px;box-shadow:2px 8px 23px 3px rgba(0,0,0,.2);overflow:hidden;font-family:Roboto,Arial,Helvetica,Verdana,sans-serif;background-color:var(--dca-gray-lightest,var(--darkened-bg,#f8f8f8));width:100%;margin:auto;left:0;right:0;margin-bottom:2em}.uip-modal .uip-modal--content .uip-modal--header{padding:15px 15px;background-color:var(--dca-white,var(--bg-color,#fff));box-shadow:0 0 8px rgba(0,0,0,.1);position:relative;z-index:1;font-size:15px;color:var(--dca-gray,var(--body-quiet-color,#666));font-weight:500;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.uip-modal .uip-modal--content .uip-modal--header .uip-modal--header-logo-title{padding-top:2px;line-height:1;text-transform:uppercase;font-weight:700;cursor:pointer}.uip-modal .uip-modal--content .uip-modal--header .uip-modal--header-close-btn{cursor:pointer}.uip-modal .uip-modal--content .uip-modal--body{font-size:12px;line-height:1.5;box-sizing:border-box;padding:0;height:70vh;display:-ms-flexbox;display:flex;min-height:50px;max-height:85vh;overflow:auto}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--sidebar{-ms-flex-negative:0;flex-shrink:0;max-width:25%}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--sidebar .uip-modal--sidebar-tabs{margin-top:30px}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--sidebar .uip-modal--sidebar-tabs .uip-modal--sidebar-tab-item{padding:15px;font-size:14px;color:var(--dca-gray,var(--body-quiet-color,#666));text-align:start;cursor:pointer;position:relative;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;text-transform:capitalize}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--sidebar .uip-modal--sidebar-tabs .uip-modal--sidebar-tab-item i{font-size:20px;padding-inline-end:15px;color:var(--dca-gray-lighter,var(--border-color,#ccc))}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--sidebar .uip-modal--sidebar-tabs .uip-modal--sidebar-tab-item img{padding-inline-end:15px}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--sidebar .uip-modal--sidebar-tabs .uip-modal--sidebar-tab-item.universal-active{background-color:var(--dca-white,var(--bg-color,#fff));box-shadow:0 6px 20px 0 rgba(0,0,0,.1)}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--sidebar .uip-modal--sidebar-tabs .uip-modal--sidebar-tab-item.universal-active:after{content:"";position:absolute;height:100%;width:5px;inset-block-start:0;inset-inline-start:0;background-color:#0bf}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--sidebar .uip-modal--sidebar-tabs .uip-modal--sidebar-tab-item.universal-active i{color:#0bf}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--sidebar .uip-modal--sidebar-tabs .uip-modal--sidebar-tab-item:only-child{display:none}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--icon-preview-wrap{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding:30px 80px 0;width:100%}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--icon-preview-wrap .uip-modal--icon-preview-inner{overflow:auto;margin:25px -15px 0;padding:0 15px 15px}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--icon-preview-wrap .uip-modal--icon-preview-inner .uip-modal--icon-preview{display:-ms-grid;display:grid;grid-gap:20px;margin:20px 0}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--icon-preview-wrap .uip-modal--icon-preview-inner .uip-modal--icon-preview .uip-icon-item{position:relative;padding:10px;background-color:var(--dca-white,var(--bg-color,#fff));box-shadow:0 1px 12px rgba(0,0,0,.05);border-radius:3px;cursor:pointer;transition:all .3s;overflow:hidden}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--icon-preview-wrap .uip-modal--icon-preview-inner .uip-modal--icon-preview .uip-icon-item:hover{box-shadow:0 1px 14px rgba(0,0,0,.16)}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--icon-preview-wrap .uip-modal--icon-preview-inner .uip-modal--icon-preview .uip-icon-item.universal-selected{box-shadow:0 1px 12px rgba(0,0,0,.05),0 0 0 3px #0bf}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--icon-preview-wrap .uip-modal--icon-preview-inner .uip-modal--icon-preview .uip-icon-item .uip-icon-item-inner{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-align:center;align-items:center;padding:1px}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--icon-preview-wrap .uip-modal--icon-preview-inner .uip-modal--icon-preview .uip-icon-item .uip-icon-item-inner .uip-icon-item__icon,.uip-modal .uip-modal--content .uip-modal--body .uip-modal--icon-preview-wrap .uip-modal--icon-preview-inner .uip-modal--icon-preview .uip-icon-item .uip-icon-item-inner i{font-size:25px;color:var(--dca-gray-darkest,var(--body-fg,#333))}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--icon-preview-wrap .uip-modal--icon-preview-inner .uip-modal--icon-preview .uip-icon-item .uip-icon-item-inner .uip-icon-item-name{color:var(--dca-gray,var(--body-quiet-color,#666));font-size:11px;padding:13px 0 0;max-width:100%;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;text-transform:capitalize}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--icon-preview-wrap .uip-modal--icon-search{position:relative}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--icon-preview-wrap .uip-modal--icon-search input{width:100%;padding:8px 15px;background-color:var(--dca-white,var(--bg-color,#fff));border:none}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--icon-preview-wrap .uip-modal--icon-search input:-ms-input-placeholder{font-style:italic}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--icon-preview-wrap .uip-modal--icon-search input::placeholder{font-style:italic}.uip-modal .uip-modal--content .uip-modal--body .uip-modal--icon-preview-wrap .uip-modal--icon-search img{position:absolute;top:50%;transform:translateY(-50%);inset-inline-end:10px}.uip-modal .uip-modal--footer{border-top:1px solid var(--dca-gray-lighter,var(--border-color,#ccc));text-align:center;background-color:var(--dca-white,var(--bg-color,#fff));border:none;display:none;-ms-flex-pack:end;justify-content:flex-end;padding:5px;box-shadow:0 0 8px rgba(0,0,0,.1);position:relative;display:-ms-flexbox;display:flex}.uip-modal .uip-modal--footer button.uip-insert-icon-button{padding:10px 35px!important;color:var(--dca-white,var(--bg-color,#fff))!important;background-color:#0bf!important;border:none;cursor:pointer;outline:0}.uip-modal .uip-modal--footer .universal-button{height:40px;margin-inline-start:5px}.uip-modal .uip-modal--footer .universal-button-success{padding:12px 36px;color:var(--dca-white,var(--bg-color,#fff));width:initial}.uip-modal .uip-modal--footer .universal-button-success:hover{background-color:#0bf}@media (min-width:1440px){body:not(.cms-admin-modal) .uip-modal .uip-modal--content{max-width:1200px}}@media (max-width:1439px){body:not(.cms-admin-modal) .uip-modal .uip-modal--content{max-width:990px}.uip-modal--icon-preview-wrap{padding:30px 50px 0}}@media (max-width:1023px){body:not(.cms-admin-modal) .uip-modal .uip-modal--content{max-width:740px}}@media (max-width:767px){.uip-modal--icon-preview-wrap{padding:15px!important}.uip-modal--sidebar{display:none}}@media (min-width:1440px){.uip-modal--icon-preview{-ms-grid-columns:(1fr)[7];grid-template-columns:repeat(7,1fr)}}@media (max-width:1439px){.uip-modal--icon-preview{-ms-grid-columns:(1fr)[6];grid-template-columns:repeat(6,1fr)}}@media (max-width:1024px){.uip-modal--icon-preview{-ms-grid-columns:(1fr)[5];grid-template-columns:repeat(5,1fr)}}@media (max-width:767px){.uip-modal--icon-preview{-ms-grid-columns:(1fr)[4];grid-template-columns:repeat(4,1fr)}}@media (max-width:479px){.uip-modal--icon-preview{-ms-grid-columns:(1fr)[3];grid-template-columns:repeat(3,1fr)}}@media (max-width:1439px){.uip-modal--sidebar-tab-item{padding:15px 15px 15px 25px;font-size:11px}.uip-modal--sidebar-tab-item i{font-size:15px}}@media (max-width:1024px){.uip-modal--sidebar-tab-item i,.uip-modal--sidebar-tab-item img{display:none}}.sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}ul.nav{margin-bottom:1em}ul.nav>li.nav-item{list-style-type:none;padding:inherit}.colM ul:not(.object-tools).nav{margin-top:0;margin-bottom:20px}ul.nav:not(:has(>li:nth-child(2))){display:none}ul.nav .nav-item{margin-inline-end:1rem}ul.nav .nav-link{position:relative;text-decoration:none}ul.nav .nav-link span.indicator{display:none;border-radius:50%;padding:.5rem;border:1px solid var(--dca-white,var(--body-bg,#fff));transform:translate(-50%,-50%);inset-block-start:0;inset-inline-start:100%;position:absolute}ul.nav .nav-link span.indicator.error{background-color:var(--bs-danger)}ul.nav .nav-link span.indicator.attributes{background-color:var(--bs-info);display:block}ul.nav .nav-link.error>span.indicator{display:block}ul.nav.nav-pills .nav-link:not(.active){border-style:solid;border-width:1px}body:not(.djangocms-admin-style) ul.djangocms-frontend.nav-tabs+div.tab-content .tab-pane{border-left-style:solid;border-bottom-style:solid;border-right-style:solid;border-left-color:var(--hairline-color);border-bottom-color:var(--hairline-color);border-right-color:var(--hairline-color);border-width:1px}body:not(.djangocms-admin-style) ul.djangocms-frontend.nav-tabs+div.tab-content .tab-pane fieldset:last-child{margin-bottom:0}div.tab-pk{-ms-flex-item-align:center;-ms-grid-row-align:center;align-self:center;color:var(--dca-gray-darker,var(--body-fg,#333));font-size:80%;margin-inline-start:auto}.djangocms-admin-style .colM ul.nav:not(.object-tools):not(.messagelist){margin-top:0}.djangocms-admin-style .colM ul.nav:not(.object-tools):not(.messagelist) li.nav-item{border-top:none}input[type=number].auto-field+span{display:none;position:absolute;inset-block-end:0;inset-inline-end:0;text-align:end;margin-inline-end:31px;margin-block-end:23px;cursor:pointer}body:not(.djangocms-admin-style) input[type=number].auto-field+span{margin-bottom:23px}@media (max-width:1024px){body:not(.djangocms-admin-style) input[type=number].auto-field+span{margin-bottom:24px}}input[type=number].auto-field+span:after{content:"auto"}input[type=number].auto-field.auto{color:var(--dca-white,var(--body-bg,#fff));caret-color:var(--dca-black,var(--body-fg,#000))}input[type=number].auto-field.auto+span{display:block} \ No newline at end of file diff --git a/djangocms_frontend/templates/djangocms_frontend.html b/djangocms_frontend/templates/djangocms_frontend.html index 0d9d974e..db9a13d2 100644 --- a/djangocms_frontend/templates/djangocms_frontend.html +++ b/djangocms_frontend/templates/djangocms_frontend.html @@ -6,9 +6,9 @@ {% block meta %} - + - + {% endblock meta %} {% block canonical_url %} diff --git a/djangocms_frontend/templates/djangocms_frontend/admin/base-tabs.html b/djangocms_frontend/templates/djangocms_frontend/admin/base-tabs.html index de155e49..481e5f48 100644 --- a/djangocms_frontend/templates/djangocms_frontend/admin/base-tabs.html +++ b/djangocms_frontend/templates/djangocms_frontend/admin/base-tabs.html @@ -7,36 +7,36 @@ {% endblock %} {% block content_subtitle %}{% endblock %} {% block field_sets %} -
- -
- {% for fieldset in adminform %} - {% if forloop.counter == 1 or "collapse" in fieldset.classes %} - {% if forloop.counter != 1 %}
{% endif %} -
- {% endif %} - {% include "djangocms_frontend/admin/includes/fieldset.html" %} - {% endfor %}{# djlint:off #}
-
+
+ +
+ {% for fieldset in adminform %} + {% if forloop.counter == 1 or "collapse" in fieldset.classes %} + {% if forloop.counter != 1 %}
{% endif %} +
+ {% endif %} + {% include "djangocms_frontend/admin/includes/fieldset.html" %} + {% endfor %}{# djlint:off #}
+
{# djlint:on #} {% endblock %} diff --git a/djangocms_frontend/contrib/component/templates/djangocms_frontend/slot.html b/djangocms_frontend/templates/djangocms_frontend/slot.html similarity index 100% rename from djangocms_frontend/contrib/component/templates/djangocms_frontend/slot.html rename to djangocms_frontend/templates/djangocms_frontend/slot.html diff --git a/djangocms_frontend/templatetags/frontend.py b/djangocms_frontend/templatetags/frontend.py index 87172e25..fa12c1a5 100644 --- a/djangocms_frontend/templatetags/frontend.py +++ b/djangocms_frontend/templatetags/frontend.py @@ -159,7 +159,7 @@ def message(self, message): return f"" if django_settings.DEBUG else "" def get_value(self, context, name, kwargs, nodelist): - from djangocms_frontend.component_pool import plugin_tag_pool + from djangocms_frontend.plugin_tag import plugin_tag_pool if name not in plugin_tag_pool: return self.message(f'Plugin "{name}" not found in pool for plugins usable with {{% plugin %}}') diff --git a/djangocms_frontend/ui_plugin_base.py b/djangocms_frontend/ui_plugin_base.py new file mode 100644 index 00000000..c2820c49 --- /dev/null +++ b/djangocms_frontend/ui_plugin_base.py @@ -0,0 +1,58 @@ +# Import the components from the current directory's models module +from cms.plugin_base import CMSPluginBase +from django.utils.encoding import force_str + +from djangocms_frontend.helpers import get_related + +try: + from cms.admin.placeholderadmin import PlaceholderAdmin + + if hasattr(PlaceholderAdmin, "edit_field"): + # FrontendEditable functionality already implemented in core? + class FrontendEditableAdminMixin: + pass + else: + # If not use our own version of the plugin-enabled mixin + raise ImportError +except ImportError: + # django CMS 3 did not implement this: use our own version of the plugin-enabled mixin + from .helpers import FrontendEditableAdminMixin + + class PlaceholderAdmin: + pass + + +class CMSUIPluginBase(FrontendEditableAdminMixin, CMSPluginBase): + render_template = "djangocms_frontend/html_container.html" + change_form_template = "djangocms_frontend/admin/base.html" + + def __str__(self): + return force_str(super().__str__()) + + def render(self, context, instance, placeholder): + for key, value in instance.config.items(): + if isinstance(value, dict) and set(value.keys()) == {"pk", "model"}: + if key not in instance.__dir__(): # hasattr would return the value in the config dict + setattr(instance.__class__, key, get_related(key)) + return super().render(context, instance, placeholder) + + if not hasattr(PlaceholderAdmin, "edit_field"): + # If the PlaceholderAdmin does not have the edit_field method, we need to provide + # the urls and the object getter here + def get_plugin_urls(self): + from django.urls import re_path + + info = f"{self.model._meta.app_label}_{self.model._meta.model_name}" + + def pat(regex, fn): + return re_path(regex, fn, name=f"{info}_{fn.__name__}") + + return [ + pat(r'edit-field/([0-9]+)/([a-z\-]+)/$', self.edit_field), + ] + super().get_plugin_urls() + + def _get_object_for_single_field(self, object_id, language): + # Method to get the object for the single field edit view + from .models import FrontendUIItem + + return FrontendUIItem.objects.get(pk=object_id) diff --git a/docs/source/components.rst b/docs/source/components.rst index d3fdf149..b2fba86a 100644 --- a/docs/source/components.rst +++ b/docs/source/components.rst @@ -342,29 +342,8 @@ to a CMS page or pages of other Django applications. They are dynamic, i.e. if the page's url changes (e.g. because it is moved in the page tree) all links pointing to the page change accordingly. -.. note:: - - **djangocms-frontend** uses django-cms' function ``get_page_choices(lang)`` - to get the list of available pages in the current language. - -The developer can extend the list of available internal link targets to pages -outside the CMS page tree using the -``DJANGOCMS_FRONTEND_LINK_MODELS`` setting in the project's ``.settings`` file. -The link/button -component can point to any page controlled by a Django model if the model class -implements the ``get_absolute_url`` method. A typical use case would, e.g., -blog entries of `djangocms-blog `_. -(This approach was inspired by mkoisten's `djangocms-styledlink -`_.) - -For more information, see -:ref:`How to add internal link targets outside of the CMS` - -.. note:: +If targets are deleted the link will fallback to regular text. - Only those destinations (outside the CMS) are shown for which a model admin - is registered and the logged in user has view permissions: A user will only - see a destination if they can view it in the admin site. Re-usable component example =========================== diff --git a/docs/source/custom_components.rst b/docs/source/custom_components.rst index 9ce61fb9..79b99ee3 100644 --- a/docs/source/custom_components.rst +++ b/docs/source/custom_components.rst @@ -1,35 +1,28 @@ -.. _custom_components: - ################# Custom Components ################# +.. index:: + single: Custom Components + .. versionadded:: 2.0 -Some frontend developers prefer custom components specifically tailored to -give the project a unique and distinct look. +Custom components are a powerful tool for content editors, allowing them to build pages without needing +in-depth knowledge of design, HTML, or nested structures. Editors can simply add content to pre-defined +components, creating visually cohesive pages with ease. When working with `Tailwind CSS `_, for example, you either create your custom components or customize components from providers, e.g. `Tailwind UI `_, -`Flowbite `_, or the commiunity +`Flowbite `_, or the community `Tailwind Components `_. -With django CMS you make your components available to the content editors for -drag and drop **and** frontend developers for use in templates from a single +With django CMS you make your components available to the content editors to +simply add them to a page by a click **and** frontend developers for use in templates from a single source. -To use custom components in your project, add -``"djangocms_frontend.contrib.component"`` to your ``INSTALLED_APPS`` setting. - -.. code-block:: python - - INSTALLED_APPS = [ - ... - "djangocms_frontend.contrib.component", - ... - ] - +Custom components are part of the djangocms-frontend root package and do not +require additional listing in the ``INSTALLED_APPS`` setting. **djangocms-frontend** will look for custom components in the ``cms_components`` module in any of your apps. This way you can @@ -45,8 +38,10 @@ Add a ``cms_components.py`` file to the ``theme`` app: .. code-block:: python # theme/cms_components.py - from djangocms_frontend.contrib.component.components import ComponentLinkMixin, CMSFrontendComponent - from djangocms_frontend.contrib.component.components import components + from djangocms_link.fields import LinkFormField + + from djangocms_frontend.component_base import CMSFrontendComponent + from djangocms_frontend.component_pool import components from djangocms_frontend.contrib.image.fields import ImageFormField @@ -77,6 +72,7 @@ Add a ``cms_components.py`` file to the ``theme`` app: allow_children = False text = forms.CharField(required=True) + link = LinkFormField() def get_short_description(self): return self.text diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index 183e0ff6..4a42dc7e 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -29,6 +29,7 @@ by adding an option: djangocms-frontend[djangocms-icon] # Installs djangocms-icon for icons support in links djangocms-frontend[static-ace] # Installs djangocms-static-ace to include the ace code editor in static files djangocms-frontend[static-ace, djangocms-icon] # comma-separate multiple dependencies + djangocms-frontend[djangocms-link] # Installs djangocms-link for link support ``djangocms-frontend[static-ace]`` is useful if your project cannot or should not access a CDN to load the `ace code editor `_ for the code plugin. @@ -44,6 +45,7 @@ Add the following entries to your ``INSTALLED_APPS``: "djangocms_icon", # optional "easy_thumbnails", + "djangocms_link", # Needed for link support "djangocms_frontend", "djangocms_frontend.contrib.accordion", "djangocms_frontend.contrib.alert", diff --git a/docs/source/grid.rst b/docs/source/grid.rst index c493c7d0..8cfa7a78 100644 --- a/docs/source/grid.rst +++ b/docs/source/grid.rst @@ -3,7 +3,13 @@ Grid plugins ############## -For details on how grids work, see, e.g. the `Bootstrap 5 documentation +The grid is the basis for responsive page design. It splits the page into +containers, rows and columns. Depending on the device, columns are shown +next to each other (larger screens) or one below the other (smaller +screens). + +The details of the grid system are based on the Bootstrap framework. Therefore, +for details on how grids work, see, e.g. the `Bootstrap 5 documentation `_. .. index:: diff --git a/docs/source/how-to/internal-link-targets.rst b/docs/source/how-to/internal-link-targets.rst index 5ebcc353..c253f4fb 100644 --- a/docs/source/how-to/internal-link-targets.rst +++ b/docs/source/how-to/internal-link-targets.rst @@ -4,109 +4,15 @@ How to add internal link targets outside the CMS ************************************************** -By default the link/button component offers available CMS pages of the -selected language as internal links. +As of version 2 of **djangocms-frontend**, the link/button plugin builds upon +the ``LinkFormField`` of djangocms-link. djangocms-link uses Django admin to +autodetect linkable models. This means that any model that has a +``get_absolute_url()`` method and a ``search_fields`` attribute in its +``ModelAdmin`` will be available as an internal link target. -The developer may extend this setting to include other page-generating -Django models as well by adding the ``DJANGOCMS_FRONTEND_LINK_MODELS`` -setting to the project's ``settings.py`` file. +The ``DJANGOCMS_FRONTEND_LINK_MODELS`` setting in djangocms-frontend before +version 2 does not have any effect anymore. -.. py:attribute:: settings.DJANGOCMS_FRONTEND_LINK_MODELS +See the README file of `djangocms-link `_ +for more information. - ``DJANGOCMS_FRONTEND_LINK_MODELS`` contains a list - of additional models that can be linked. - - Each model is specified within its own dict. The resulting drop-down - list will contain objects grouped by their type. The order of the types - in the list is defined by the order of their definition in this setting. - - The only required attribute for each model is ``class_path``, which must - be the full python path to the model. - - Additional attributes are: - - ``type``: - This is the name that will appear in the grouped dropdown menu. If - not specified, the name of the class will be used E.g., "``Page``". - - ``filter``: - You can specify additional filtering rules here. This must be - specified as a dict but is converted directly into kwargs internally, - so, ``{'published': True}`` becomes ``filter(published=True)`` for - example. - - ``order_by``: - Specify the ordering of any found objects exactly as you would in a - queryset. If this is not provided, the objects will be ordered in the - natural order of your model, if any. - - ``search``: - Specifies which (text) field of the model should be searched when - the user types a search string. - -.. note:: - - Each of the defined models must define a ``get_absolute_url()`` - method on its objects or the configuration will be rejected. - -Example for a configuration that allows linking CMS pages plus two -different page types from two djangocms-blog apps called "Blog" and -"Content hub" (having the ``app_config_id`` 1 and 2, respectively): - -.. code:: python - - DJANGOCMS_FRONTEND_LINK_MODELS = [ - { - "type": _("Blog pages"), - "class_path": "djangocms_blog.models.Post", - "filter": {"publish": True, "app_config_id": 1}, - "search": "translations__title", - }, - { - "type": _("Content hub pages"), - "class_path": "djangocms_blog.models.Post", - "filter": {"publish": True, "app_config_id": 2}, - "search": "translations__title", - }, - ] - -Another example might be (taken from djangocms-styledlink -documentation): - -.. code:: python - - DJANGOCMS_FRONTEND_LINK_MODELS = [ - { - 'type': 'Clients', - 'class_path': 'myapp.Client', - 'manager_method': 'published', - 'order_by': 'title' - }, - { - 'type': 'Projects', - 'class_path': 'myapp.Project', - 'filter': { 'approved': True }, - 'order_by': 'title', - }, - { - 'type': 'Solutions', - 'class_path': 'myapp.Solution', - 'filter': { 'published': True }, - 'order_by': 'name', - } - ] - -The link/button plugin uses select2 to show all available link targets. -This allows you to search the page titles. - -.. warning:: - - If you have a huge number (> 1,000) of link target (i.e., pages or - blog entries or whatever) the current implementation might slow down - the editing process. In your ``settings`` file you can set - ``DJANGOCMS_FRONTEND_MINIMUM_INPUT_LENGTH`` to a value greater than 1 and - **djangocms-frontend** will wait until the user inputs at least this many - characters before querying potential link targets. - -.. index:: - single: Extend plugins diff --git a/docs/source/index.rst b/docs/source/index.rst index d1e7257b..b0e2ce3c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,9 +1,3 @@ -.. - djangocms-blog documentation master file, created by - sphinx-quickstart on Sun Jun 5 23:27:04 2016. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - ################################################ Welcome to djangocms-frontend's documentation! ################################################ @@ -12,11 +6,12 @@ djangocms-frontend ******************** -**djangocms-frontend** is a plugin bundle based on `djangocms_bootstrap5 -`_. Its objective is to -provide a set of popular frontend components independent of the -currently used frontend framework such as Bootstrap, or its specific -version. +**django CMS Frontend** is a plugin bundle which originally built on and improved +the architecture of `djangocms-bootstrap4 `_. +Its objective is to provide a toolset to quickly create re-usable frontend +components and comes preloaded with a set of popular frontend components +independent of the currently used frontend framework such as Bootstrap, or +its specific version. .. image:: ../../preview.png @@ -24,27 +19,28 @@ version. Key features ************** -- Support of `Bootstrap 5 `_. +- **Easy to implement re-usable frontend custom components**, which in the + simplest case consist of a template and declarative sort of form class. -- **Separation of plugins from css framework**, i.e., no need to - rebuild you site's plugin tree if css framework is changed in the - future, e.g., from Bootstrap 5 to a future version. +- Support of `Bootstrap 5 `_, django CMS 3.8+ + and django CMS 4 out of the box. -- **New link plugin** allowing to link to internal pages provided by - other applications, such as `djangocms-blog - `_. +- **Separation of plugins from css framework**, i.e. no need to + rebuild you site's plugin tree if css framework is changed in the + future, e.g. from Bootstrap 5 to a future version. -- **Nice and well-arranged admin frontend** of `djangocms-bootstrap4 - `_ +- Leverage of new **djangocms-link features** allowing to link to internal pages + provided by other applications, such as `djangocms-blog + `_. -- Management command to **migrate from djangocms-bootstrap4**. This - command automatically migrates all djangocms-bootstrap4 plugins to - djangocms-frontend. +- **Nice and well-arranged admin frontend** of djangocms-bootstrap4 -- **Extensible** within the project and with separate project (e.g., a - theme app) +- **Extensible** within the project and with separate project (e.g. a + theme app). Create your own components with a few lines of code only. -- **Accordion** plugin. +- **Plugins are re-usable as UI components** anywhere in your project + (e.g. in a custom app) giving your whole project a more consistent + user experience. ************* Description @@ -62,10 +58,6 @@ Instead all design parameters are stored in a common JSON field and future releases of improved frontend features will not require to rebuild your full plugin tree. -The link plugin has been rewritten to not allow internal links to other -CMS pages, but also to other django models such as, e.g., posts of -`djangocms-blog `_. - **djangocms-frontend** provides a set of plugins to structure your layout. This includes three basic elements @@ -80,10 +72,6 @@ Components to grasp and easy to use look. Alerts or cards are examples of components. -Forms - To nicely integrate formss into your page we recommend - **djangocms-form-builder** which works stand-alone but also nicely - integrates with **djangocms-frontend**. Contents ======== @@ -92,9 +80,9 @@ Contents :maxdepth: 3 getting_started + custom_components grid components - custom_components how-to/index reference diff --git a/private/sass/components/_tabs.scss b/private/sass/components/_tabs.scss index 1be37e14..b4244f2c 100644 --- a/private/sass/components/_tabs.scss +++ b/private/sass/components/_tabs.scss @@ -4,6 +4,10 @@ } ul.nav { + &:not(:has(> li:nth-child(2))) { + // Hide single tab + display: none; + } .nav-item { margin-inline-end: 1rem; } diff --git a/pyproject.toml b/pyproject.toml index 2b83d87c..c8c183e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,35 +1,126 @@ [tool.black] +exclude = ''' + .git + .hg + .tox + venv + .venv + _build + build + dist +''' + +[tool.ruff] +line-length = 119 exclude = [ - ".git", - ".hg", - ".tox", - "venv", - ".venv", - "_build", - "build", - "dist" + "**/migrations/**", + ".env", + ".venv", +] +lint.select = [ + "E", + "F", + "I", + "W", ] -[tool.djlint] -ignore="H023,T003,H006,H031,D018,H021,H025" -profile="django" +lint.ignore = [ + "E501", # line too long + "E701", # multiple statements on one line (colon) + "F401", # module imported but unused + "F403", # 'from module import *' used; unable to detect undefined names +] -[tool.ruff] +[tool.isort] +line_length = 119 +skip = [ + "manage.py", + "*migrations*", + ".tox", + ".eggs", + ".env", + ".venv", + "data", +] +include_trailing_comma = true +multi_line_output = 5 +lines_after_imports = 2 +default_section = "THIRDPARTY" +sections = [ + "FUTURE", + "STDLIB", + "DJANGO", + "CMS", + "THIRDPARTY", + "FIRSTPARTY", + "LOCALFOLDER", +] +known_first_party = "djangocms_link" +known_cms = [ "cms", "menus" ] +known_django = "django" + +[tool.flake8] +max-line-length = 120 +select = [ + "C", + "E", + "F", + "W", + "B", + "B950", +] +ignore = [ + "W503", # line break before binary operator +] exclude = [ - ".env", - ".venv", - "**/migrations/**", + "*.egg-info", + ".eggs", + ".env", + ".git", + ".settings", + ".tox", + ".venv", + "build", + "data", + "dist", + "docs", + "*migrations*", + "requirements", + "tmp", ] -lint.ignore = [ - "E501", # line too long - "F403", # 'from module import *' used; unable to detect undefined names - "E701", # multiple statements on one line (colon) - "F401", # module imported but unused + +[tool.coverage.run] +branch = true +source = [ "djangocms_frontend" ] +omit = [ + "djangocms_frontend/migrations/*", + "djangocms_frontend/management/*", + "*/foundation6.py", + "tests/*", ] -line-length = 119 -lint.select = [ - "I", - "E", - "F", - "W", + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "if self.debug:", + "if settings.DEBUG", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + "if __name__ == .__main__.:", +] +ignore_errors = true + +[tool.djlint] +ignore = "H023,T003,H006,H031,D018,H021,H025" +profile = "django" + +[tool.djlint.rule] +name = "D034" +message = ".pk or .id should only be used with \"|unlocalize\"" +flags = "re.DOTALL|re.I" +patterns = [ + '{{\s*[a-zA-Z0-9_.]+\.pk\s*}}', + '{{\s*[a-zA-Z0-9_.]+\.id\s*}}', ] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 506f1c46..00000000 --- a/setup.cfg +++ /dev/null @@ -1,33 +0,0 @@ -[flake8] -dictionaries=en_US,python,technical,django -ignore = D203, W503 -select = C,E,F,W,B,B950 -extend-ignore = E203, E501, E731 -max-line-length = 119 -exclude = - *.egg-info, - .eggs, - .git, - .settings, - .tox, - build, - data, - dist, - docs, - *migrations*, - requirements, - tmp - -[isort] -profile = black -;line_length = 119 -;skip = manage.py, *migrations*, .tox, .eggs, data -;include_trailing_comma = true -;multi_line_output = 5 -;not_skip = __init__.py -;lines_after_imports = 2 -;default_section = THIRDPARTY -;sections = FUTURE, STDLIB, DJANGO, CMS, THIRDPARTY, FIRSTPARTY, LIB, LOCALFOLDER -;known_first_party = djangocms_frontend -;known_cms = cms, menus -;known_django = django diff --git a/setup.py b/setup.py index 1e49003b..0109b7f8 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,8 @@ "django-cms>=3.7", "django-filer>=1.7", "easy-thumbnails", - "djangocms-attributes-field>=1", + "djangocms-attributes-field>=4", + "djangocms-link>=5", "django-entangled>=0.6", ] @@ -18,8 +19,12 @@ "static-ace": [ "djangocms-static-ace", ], + "djangocms-link": [ + "djangocms-link>=5.0.0", + ], "cms-4": [ "django-cms>=4.1.0", + "djangocms-link>=5.0.0", "django-parler", "djangocms-versioning>=2.0.0", "djangocms-alias>=2.0.0", @@ -28,6 +33,7 @@ "cms-3": [ "django-cms<4", "djangocms-text", + "djangocms-link>=5.0.0", "django-parler", ], } diff --git a/tests/component/test_models.py b/tests/component/test_models.py index c1fa79c8..001e0d79 100644 --- a/tests/component/test_models.py +++ b/tests/component/test_models.py @@ -1,6 +1,6 @@ from django.test import TestCase -from djangocms_frontend.contrib.component.registry import components +from djangocms_frontend.component_pool import components class ComponentPluginTestCase(TestCase): diff --git a/tests/component/test_plugins.py b/tests/component/test_plugins.py index 61d175d6..3368d341 100644 --- a/tests/component/test_plugins.py +++ b/tests/component/test_plugins.py @@ -1,12 +1,12 @@ from cms.api import add_plugin from cms.test_utils.testcases import CMSTestCase -from djangocms_frontend.contrib.alert.cms_plugins import AlertPlugin -from djangocms_frontend.contrib.component.cms_plugins import ( +from djangocms_frontend.cms_plugins import ( MyButtonPlugin, MyHeroPlugin, MyStrangeComponentPlugin, ) +from djangocms_frontend.contrib.alert.cms_plugins import AlertPlugin from ..fixtures import TestFixture @@ -54,7 +54,7 @@ def test_component_with_empty_slots_plugin(self): ) def test_component_with_slots_plugin(self): - from djangocms_frontend.contrib.component.cms_plugins import MyHeroSlotPlugin + from djangocms_frontend.cms_plugins import MyHeroSlotPlugin instance = add_plugin( placeholder=self.placeholder, @@ -119,10 +119,7 @@ def test_component_with_slots_plugin(self): ) def test_autocreate_slots(self): - from djangocms_frontend.contrib.component.cms_plugins import ( - MyHeroSlotPlugin, - MyHeroTitlePlugin, - ) + from djangocms_frontend.cms_plugins import MyHeroSlotPlugin, MyHeroTitlePlugin instance = add_plugin( placeholder=self.placeholder, @@ -149,10 +146,11 @@ def test_simple_component_plugin(self): language=self.language, ) instance.initialize_from_form(MyButtonPlugin.form) - instance.config["internal_link"] = {"model": "cms.page", "pk": self.page.pk} + instance.config["link"] = {"internal_link": f"cms.page:{self.page.pk}"} instance.save() - link = instance.get_link() + from djangocms_link.templatetags.djangocms_link_tags import to_url + link = to_url(instance.link) self.publish(self.page, self.language) diff --git a/tests/fixtures.py b/tests/fixtures.py index a041b842..6984ffc9 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -97,7 +97,6 @@ def create_url( ): from djangocms_url_manager.models import Url, UrlGrouper from djangocms_url_manager.utils import is_versioning_enabled - from djangocms_versioning.constants import DRAFT from djangocms_versioning.models import Version if site is None: @@ -114,10 +113,12 @@ def create_url( url_grouper=UrlGrouper.objects.create(), ) if is_versioning_enabled(): + from djangocms_versioning.constants import PUBLISHED + Version.objects.create( content=url, created_by=self.superuser, - state=DRAFT, + state=PUBLISHED, content_type_id=ContentType.objects.get_for_model(Url).id, ) diff --git a/tests/link/test_migration.py b/tests/link/test_migration.py new file mode 100644 index 00000000..77e4104e --- /dev/null +++ b/tests/link/test_migration.py @@ -0,0 +1,27 @@ +from django.test.testcases import TestCase + + +class LinkMigrationTestCase(TestCase): + def setUp(self): + import importlib + + link_migration = importlib.import_module("djangocms_frontend.migrations.0002_migrate_links") + self.convert = staticmethod(link_migration.convert_item) + + def test_link_migration(self): + test_links = ( + dict(external_link="https://www.django-cms.com"), + dict(external_link="https://www.django-cms.com", anchor="top"), + dict(internal_link=dict(model="cms.page", pk=1)), + dict(internal_link=dict(model="cms.page", pk=1), anchor="top"), + dict(file_link=dict(model="filer.file", pk=1)), + dict(phone="1234567890"), + dict(mailto="mail@example.com") + ) + + for link in test_links: + with self.subTest(link=link): + config = link.copy() + self.convert(config, "forward") + self.convert(config, "backward") + self.assertEqual(link, config) diff --git a/tests/link/test_models.py b/tests/link/test_models.py index c7da7bcc..6d193745 100644 --- a/tests/link/test_models.py +++ b/tests/link/test_models.py @@ -6,7 +6,7 @@ class LinkModelTestCase(TestCase): def test_instance(self): instance = Link.objects.create( - config=dict(name="Get it!", external_link="https://www.django-cms.com/") + config=dict(name="Get it!", link=dict(external_link="https://www.django-cms.com/")) ) self.assertEqual(str(instance), "Link (1)") self.assertEqual( diff --git a/tests/link/test_plugins.py b/tests/link/test_plugins.py index 62646e82..02030c37 100644 --- a/tests/link/test_plugins.py +++ b/tests/link/test_plugins.py @@ -1,12 +1,10 @@ from cms.api import add_plugin from cms.test_utils.testcases import CMSTestCase -from cms.utils.urlutils import admin_reverse from django.http import HttpRequest from djangocms_frontend import settings from djangocms_frontend.contrib.link.cms_plugins import TextLinkPlugin -from djangocms_frontend.contrib.link.forms import LinkForm, SmartLinkField -from djangocms_frontend.contrib.link.helpers import get_choices +from djangocms_frontend.contrib.link.forms import LinkForm from ..fixtures import TestFixture @@ -18,7 +16,7 @@ def test_plugin(self): plugin_type=TextLinkPlugin.__name__, language=self.language, config=dict( - external_link="https://www.divio.com", + link=dict(external_link="https://www.divio.com"), ), ).initialize_from_form(LinkForm).save() self.publish(self.page, self.language) @@ -34,7 +32,7 @@ def test_plugin(self): plugin_type=TextLinkPlugin.__name__, language=self.language, config=dict( - external_link="https://www.divio.com", + dict(link=dict(external_link="https://www.divio.com")), link_context="primary", link_size="btn-sm", link_block=True, @@ -57,7 +55,7 @@ def test_plugin(self): plugin_type=TextLinkPlugin.__name__, language=self.language, config=dict( - internal_link=dict(model="cms.page", pk=self.page.id), + link=dict(internal_link=f"cms.page:{self.page.id}"), link_context="primary", link_type="btn", name="django CMS rocks!", @@ -71,8 +69,6 @@ def test_plugin(self): self.assertEqual(response.status_code, 200) self.assertContains(response, "btn-primary") self.assertContains(response, 'href="/content/"') - # Finally, test the descriptor - self.assertEqual(plugin.internal_link, self.page) # alternate version broken link plugin = add_plugin( @@ -101,7 +97,7 @@ def test_plugin(self): plugin_type=TextLinkPlugin.__name__, language=self.language, config=dict( - external_link="https://www.divio.com", + link=dict(external_link="https://www.divio.com"), link_context="primary", link_type="btn", link_outline=True, @@ -140,7 +136,10 @@ def test_link_form(self): ) ) if hasattr(self, "create_url") - else dict(external_link="https://www.django-cms.org/") + else dict( + link_0="external_link", + link_1="https://www.django-cms.org/", + ) ), } ) @@ -148,50 +147,3 @@ def test_link_form(self): self.assertTrue(form.is_valid(), f"{form.__class__.__name__}:form errors: {form.errors}") if hasattr(self, "create_url"): self.delete_urls() - else: - request.POST.update({"mailto": "none@nowhere.com"}) - form = LinkForm(request.POST) - self.assertFalse(form.is_valid()) # Two targets - request.POST["external_link"] = None - form = LinkForm(request.POST) - self.assertFalse(form.is_valid()) # no anchor for mail - - -class AutocompleteViewTestCase(TestFixture, CMSTestCase): - - def test_smart_link_field(self): - slf = SmartLinkField() - choices = get_choices(None) - self.assertEqual("example.com", choices[0][0]) # Site name - self.assertIn(("2-1", "home"), choices[0][1]) - - cleaned = slf.clean("2-1") - self.assertEqual(dict(model="cms.page", pk=1), cleaned) - - self.assertEqual(slf.prepare_value("blabla"), "") - self.assertEqual(slf.prepare_value(dict(model="cms.page", pk=1)), "2-1") - self.assertEqual(slf.prepare_value(self.home), "2-1") - - def test_autocomplete_view(self): - tricky_title = """d'acceuil: """ - page = self.create_page( - title=tricky_title, - template="page.html", - ) - expected_choices = [ - "home", "content", tricky_title, - ] - - self.publish(page, self.language) - autocomplete_url = admin_reverse("link_link_autocomplete") - - with self.login_user_context(self.superuser): - response = self.client.get(autocomplete_url) - - autocomplete_result = response.json() - choices = autocomplete_result.get("results")[0] - - self.assertFalse((autocomplete_result.get("pagination") or {}).get("more")) - - for expected, sent in zip(expected_choices, choices.get("children")): - self.assertEqual(expected, sent.get("text")) diff --git a/tests/link/test_urlconf.py b/tests/link/test_urlconf.py deleted file mode 100644 index b90454ce..00000000 --- a/tests/link/test_urlconf.py +++ /dev/null @@ -1,16 +0,0 @@ -from unittest import skipIf - -from cms.test_utils.testcases import CMSTestCase -from cms.utils.urlutils import admin_reverse -from django.urls import NoReverseMatch - -from tests.fixtures import DJANGO_CMS4, TestFixture - - -@skipIf(DJANGO_CMS4, "Tests for django CMS 4 use URL manager with own select2") -class TestUrlConfTestCase(TestFixture, CMSTestCase): - def test_select2_url_reversible(self): - try: - admin_reverse("link_link_autocomplete") - except NoReverseMatch as e: - self.fail(str(e)) diff --git a/tests/navigation/test_plugins.py b/tests/navigation/test_plugins.py index 0677c2e0..b6739c9c 100644 --- a/tests/navigation/test_plugins.py +++ b/tests/navigation/test_plugins.py @@ -57,7 +57,7 @@ def test_plugin(self): language=self.language, target=nav, config=dict( - internal_link=dict(model="cms.page", pk=self.page.id), + link=dict(internal_link=f"cms.page:{self.page.id}"), link_context="primary", link_type="btn", name="django CMS rocks!", diff --git a/tests/requirements/base.txt b/tests/requirements/base.txt index 9f16ee5d..ba833b92 100644 --- a/tests/requirements/base.txt +++ b/tests/requirements/base.txt @@ -9,5 +9,8 @@ wheel black pre-commit djangocms-text +django-filer html5lib -. +-e . +pytest +pytest-django diff --git a/tests/test_app/cms_components.py b/tests/test_app/cms_components.py index 20890102..ba7e5721 100644 --- a/tests/test_app/cms_components.py +++ b/tests/test_app/cms_components.py @@ -1,10 +1,8 @@ from django import forms +from djangocms_link.fields import LinkFormField -from djangocms_frontend.contrib.component.components import ( - CMSFrontendComponent, - ComponentLinkMixin, -) -from djangocms_frontend.contrib.component.registry import components +from djangocms_frontend.component_base import CMSFrontendComponent +from djangocms_frontend.component_pool import components from djangocms_frontend.contrib.image.fields import ImageFormField @@ -33,13 +31,14 @@ def get_short_description(self): @components.register -class MyButton(ComponentLinkMixin, CMSFrontendComponent): +class MyButton(CMSFrontendComponent): class Meta: name = "Button" render_template = "button.html" allow_children = False text = forms.CharField(required=True, initial="Click me") + link = LinkFormField() def get_short_description(self): return self.text diff --git a/tests/test_app/templates/button.html b/tests/test_app/templates/button.html index 218fe92a..c145c6eb 100644 --- a/tests/test_app/templates/button.html +++ b/tests/test_app/templates/button.html @@ -1 +1 @@ -{{ instance.text }} +{% load djangocms_link_tags %}{{ instance.text }} diff --git a/tests/test_plugin_tag.py b/tests/test_plugin_tag.py index 4031f9db..1d01cb3d 100644 --- a/tests/test_plugin_tag.py +++ b/tests/test_plugin_tag.py @@ -28,7 +28,6 @@ def test_tag_rendering_with_paramter(self): expected_result = """""" - result = template.render({"request": None}) self.assertInHTML(expected_result, result) @@ -72,7 +71,6 @@ def test_complex_tags(self):
A third item
""" result = template.render({"request": None}) - self.assertInHTML(expected_result, result) def test_link_plugin(self): @@ -82,19 +80,18 @@ def test_link_plugin(self): {% plugin "textlink" name="Click" url_grouper=grouper site=test_site link_type="btn" link_context="primary" link_outline=False %} Click me! {% endplugin %} - """) # noqa: B950 + """) # noqa: B950,E501 else: grouper = None - template = django_engine.from_string("""{% load frontend %} - {% plugin "textlink" name="Click" external_link="/test/" link_type="btn" link_context="primary" link_outline=False %} + template = django_engine.from_string("""{% load frontend djangocms_link_tags %}{{ "test"|to_link }} + {% plugin "textlink" name="Click" link="/test/"|to_link link_type="btn" link_context="primary" link_outline=False %} Click me! {% endplugin %} - """) # noqa: B950 + """) # noqa: B950,E501 expected_result = """Click me!""" result = template.render({"request": None, "test_site": get_current_site(None), "grouper": grouper}) - self.assertInHTML(expected_result, result) @override_settings(DEBUG=True) diff --git a/tests/test_settings.py b/tests/test_settings.py index b7645be9..68ea56b6 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -15,6 +15,7 @@ "menus", "treebeard", "djangocms_text", + "djangocms_link", "djangocms_frontend", "djangocms_frontend.contrib.accordion", "djangocms_frontend.contrib.alert", @@ -22,7 +23,6 @@ "djangocms_frontend.contrib.card", "djangocms_frontend.contrib.carousel", "djangocms_frontend.contrib.collapse", - "djangocms_frontend.contrib.component", "djangocms_frontend.contrib.content", "djangocms_frontend.contrib.grid", "djangocms_frontend.contrib.icon", diff --git a/tests/utilities/test_plugins.py b/tests/utilities/test_plugins.py index ee3f6987..4e7b3915 100644 --- a/tests/utilities/test_plugins.py +++ b/tests/utilities/test_plugins.py @@ -8,6 +8,7 @@ TOCPlugin, ) from djangocms_frontend.contrib.utilities.forms import TableOfContentsForm +from djangocms_frontend.ui_plugin_base import PlaceholderAdmin from ..fixtures import TestFixture @@ -100,8 +101,12 @@ def test_heading_inline_endpoint(self): "heading_id": "id1", }, ) - url_endpoint = admin_reverse("utilities_heading_edit_field", args=[heading.pk, self.language]) - url_endpoint += "?edit_fields=heading" + + if hasattr(PlaceholderAdmin, "edit_field"): + url_endpoint = "cms_placeholder_edit_field" + else: + url_endpoint = "utilities_heading_edit_field" + url_endpoint = admin_reverse(url_endpoint, args=[heading.pk, self.language]) + "?edit_fields=heading" data = { "heading": "My new heading", }