Skip to content

Commit

Permalink
Add support for djangocms-link 5
Browse files Browse the repository at this point in the history
  • Loading branch information
fsbraun committed Oct 27, 2024
1 parent 1c0173e commit 981c00c
Show file tree
Hide file tree
Showing 18 changed files with 47 additions and 249 deletions.
11 changes: 5 additions & 6 deletions djangocms_frontend/common/bootstrap5/background.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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")
Expand Down Expand Up @@ -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)"),
)
Expand Down
2 changes: 1 addition & 1 deletion djangocms_frontend/common/bootstrap5/responsive.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions djangocms_frontend/common/title.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <code>title</code> attribute "
Expand Down
20 changes: 4 additions & 16 deletions djangocms_frontend/contrib/link/cms_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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:
Expand All @@ -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):
Expand All @@ -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/
Expand Down
126 changes: 9 additions & 117 deletions djangocms_frontend/contrib/link/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand Down Expand Up @@ -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 <em>not</em> include a preceding "&#35;" 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,
Expand All @@ -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):
Expand Down
1 change: 0 additions & 1 deletion djangocms_frontend/contrib/link/frameworks/bootstrap5.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
96 changes: 7 additions & 89 deletions djangocms_frontend/contrib/link/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 ""
Loading

0 comments on commit 981c00c

Please sign in to comment.