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",