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/link/cms_plugins.py b/djangocms_frontend/contrib/link/cms_plugins.py index dd6c0564..99f80bf5 100644 --- a/djangocms_frontend/contrib/link/cms_plugins.py +++ b/djangocms_frontend/contrib/link/cms_plugins.py @@ -12,13 +12,14 @@ from .. import link from . import forms, models, views 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 +34,6 @@ }, ), ] -if not apps.is_installed("djangocms_url_manager"): - UILINK_FIELDSET += [ - ( - _("Link settings"), - { - "classes": ("collapse",), - "fields": ( - ("mailto", "phone"), - ("anchor", "target"), - ("file_link",), - ), - }, - ), - ] class LinkPluginMixin: @@ -65,6 +52,7 @@ class LinkPluginMixin: 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 +73,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/ diff --git a/djangocms_frontend/contrib/link/forms.py b/djangocms_frontend/contrib/link/forms.py index f7e36efd..5936a3ce 100644 --- a/djangocms_frontend/contrib/link/forms.py +++ b/djangocms_frontend/contrib/link/forms.py @@ -7,17 +7,14 @@ 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.core.exceptions import ObjectDoesNotExist 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 ... import settings from ...common import SpacingFormMixin @@ -28,11 +25,11 @@ TagTypeFormField, TemplateChoiceMixin, ) -from ...helpers import first_choice, get_related_object +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 +from .helpers import get_object_for_value mixin_factory = settings.get_forms(link) @@ -174,59 +171,18 @@ class AbstractLinkForm(EntangledModelForm): 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"), + link = LinkFormField( + label=_("Link"), + initial={}, required=False, - help_text=_("If provided links a file from the filer app."), ) - # other link types - anchor = forms.CharField( - label=_("Anchor"), - 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 +191,7 @@ 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) - - 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, - } - ) + self.fields["link"].required = not self.link_is_optional 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..04a1864c 100644 --- a/djangocms_frontend/contrib/link/helpers.py +++ b/djangocms_frontend/contrib/link/helpers.py @@ -2,6 +2,7 @@ from cms.forms.utils import get_page_choices from cms.models import Page +from django.apps import apps from django.conf import settings as django_settings from django.contrib.admin import site from django.contrib.contenttypes.models import ContentType @@ -138,99 +139,16 @@ def to_choices(json): 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/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/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/setup.py b/setup.py index 1e49003b..20d7727e 100644 --- a/setup.py +++ b/setup.py @@ -18,8 +18,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 +32,7 @@ "cms-3": [ "django-cms<4", "djangocms-text", + "djangocms-link>=5.0.0", "django-parler", ], } diff --git a/tests/component/test_plugins.py b/tests/component/test_plugins.py index 61d175d6..ec5570d9 100644 --- a/tests/component/test_plugins.py +++ b/tests/component/test_plugins.py @@ -149,7 +149,7 @@ 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() diff --git a/tests/fixtures.py b/tests/fixtures.py index a041b842..216590d3 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -2,6 +2,7 @@ from django.apps import apps from django.contrib.contenttypes.models import ContentType from django.contrib.sites.models import Site +from djangocms_versioning.constants import PUBLISHED DJANGO_CMS4 = apps.is_installed("djangocms_versioning") @@ -97,7 +98,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: @@ -117,7 +117,7 @@ def create_url( 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_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..e15b4234 100644 --- a/tests/link/test_plugins.py +++ b/tests/link/test_plugins.py @@ -18,7 +18,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 +34,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 +57,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 +71,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 +99,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, 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..601ba32b 100644 --- a/tests/requirements/base.txt +++ b/tests/requirements/base.txt @@ -10,4 +10,4 @@ black pre-commit djangocms-text html5lib -. +djangocms-link>=5.0.0 diff --git a/tests/test_plugin_tag.py b/tests/test_plugin_tag.py index 4031f9db..e1f074e3 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): @@ -85,8 +83,8 @@ def test_link_plugin(self): """) # noqa: B950 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 @@ -94,7 +92,6 @@ def test_link_plugin(self): 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..96e1511a 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",