Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,13 @@ cached.)::
obj = MyModel.objects.first()
url = obj.link.url

A ``LinkField`` used inside a CMS plugin will automatically show internal link targets in
the language of the plugin (which might differ from the edit dialog's language). This
follows the principle the all content in the dialog is shown in the object's language.

If you use ``LinkField`` in your own models, the default will be the current language.


Link models
-----------

Expand Down
26 changes: 16 additions & 10 deletions djangocms_link/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,17 +139,17 @@ def get_reference(self, request: HttpRequest) -> JsonResponse:
obj = get_manager(model).get(pk=pk)
if model_str == "cms.page":
obj.__link_text__ = obj.get_admin_content(language, fallback=True).title
return JsonResponse(self.serialize_result(obj))
return JsonResponse(self.serialize_result(obj) or {})
elif model_str == "cms.page":
obj = get_manager(model).get(pk=pk)
obj.__link_text__ = obj.get_title(language, fallback=True)
return JsonResponse(self.serialize_result(obj))
return JsonResponse(self.serialize_result(obj) or {})

if hasattr(model_admin, "get_link_queryset"):
obj = model_admin.get_link_queryset(self.request, None).get(pk=pk)
else:
obj = model_admin.get_queryset(self.request).get(pk=pk)
return JsonResponse(self.serialize_result(obj))
return JsonResponse(self.serialize_result(obj) or {})
except Exception as e:
return JsonResponse({"error": str(e)})

Expand All @@ -166,7 +166,9 @@ def get_optgroups(self, context):
"text": previous_model.capitalize(),
"children": [],
}
model["children"].append(self.serialize_result(obj))
data = self.serialize_result(obj)
if data: # Only append if serialization was successful
model["children"].append(data)
if model:
results.append(model)
return results
Expand All @@ -181,12 +183,16 @@ def serialize_result(self, obj: Model) -> dict:
obj.__link_text__ = obj.get_admin_content(self.language).title

indentation = UNICODE_SPACE * (max(getattr(obj, "__depth__", 1), 1) - 1)
return {
"id": f"{obj._meta.app_label}.{obj._meta.model_name}:{obj.pk}",
"text": indentation + (getattr(obj, "__link_text__", str(obj)) or str(obj)),
"url": obj.get_absolute_url(),
"verbose_name": str(obj._meta.verbose_name).capitalize(),
}
url = obj.get_absolute_url()
text = getattr(obj, "__link_text__", str(obj)) or str(obj)
if url and text:
return {
"id": f"{obj._meta.app_label}.{obj._meta.model_name}:{obj.pk}",
"text": indentation + text,
"url": url,
"verbose_name": str(obj._meta.verbose_name).capitalize(),
}
return None

def get_queryset(self) -> QuerySet:
"""Return queryset based on ModelAdmin.get_search_results()."""
Expand Down
28 changes: 28 additions & 0 deletions djangocms_link/cms_plugins.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,41 @@
from __future__ import annotations

from django.contrib.sites.shortcuts import get_current_site
from django.db import models
from django.utils.translation import gettext_lazy as _

from cms.plugin_base import CMSPluginBase
from cms.plugin_pool import plugin_pool

from djangocms_link.fields import LinkFormField

from .helpers import get_link
from .models import Link


def patch(original: callable) -> callable:
"""
Patch the get_form method of CMSPluginBase to ensure that the
'language' attribute is set on the link widget's subwidgets in the form.
This is necessary for the link field to work correctly with multilingual sites and
shows the internal link target in the correct plugin language.
"""
def get_form(self, request, obj: models.Model | None = None, change: bool = False, **kwargs):
form = original(self, request, obj=obj, change=change, **kwargs)

language = getattr(obj, "language", None) if obj else request.GET.get("plugin_language", None)
for field in form.base_fields.values():
if isinstance(field, LinkFormField):
for widget in field.widget.widgets:
widget.language = language
return form

return get_form


CMSPluginBase.get_form = patch(CMSPluginBase.get_form)


class LinkPlugin(CMSPluginBase):
model = Link
name = _("Link")
Expand Down
73 changes: 40 additions & 33 deletions djangocms_link/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
from django.db import models
from django.db.models import JSONField, ManyToOneRel
from django.forms import Field, MultiWidget, Select, TextInput, URLInput
from django.utils.encoding import force_str
from django.utils.translation import get_language
from django.utils.translation import gettext_lazy as _
from django.utils.translation import override

from cms.utils.urlutils import admin_reverse

Expand All @@ -31,8 +33,9 @@


class LinkAutoCompleteWidget(AutocompleteSelect):
def __init__(self, attrs: dict | None = None):
def __init__(self, attrs: dict | None = None, language: str | None = None):
super().__init__(None, None, attrs)
self.language = language

def get_internal_obj(self, values: list[str | None]) -> list[models.Model | None]:
internal_obj = []
Expand All @@ -46,29 +49,31 @@ def get_internal_obj(self, values: list[str | None]) -> list[models.Model | None
return internal_obj

def optgroups(self, name: str, value: str, attr: str | None = None):
default = (None, [], 0)
groups = [default]
has_selected = False
selected_choices = set(value)
if not self.is_required and not self.allow_multiple_selected:
default[1].append(self.create_option(name, "", "", False, 0))

for option_value, option_label in zip(value, self.get_internal_obj(value)):
selected = str(option_value) in value and (
has_selected is False or self.allow_multiple_selected
)
has_selected |= selected
index = len(default[1])
subgroup = default[1]
subgroup.append(
self.create_option(
name, option_value, option_label, selected_choices, index
with override(self.language or get_language()):
default = (None, [], 0)
groups = [default]
has_selected = False
selected_choices = set(value)
if not self.is_required and not self.allow_multiple_selected:
default[1].append(self.create_option(name, "", "", False, 0))

for option_value, option_label in zip(value, self.get_internal_obj(value)):
selected = str(option_value) in value and (
has_selected is False or self.allow_multiple_selected
)
has_selected |= selected
index = len(default[1])
subgroup = default[1]
subgroup.append(
self.create_option(
name, option_value, force_str(option_label), selected_choices, index
)
)
)
return groups

def get_url(self):
return admin_reverse("djangocms_link_link_urls")
reverse = admin_reverse("djangocms_link_link_urls")
return f"{reverse}?language={self.language}" if self.language else reverse

def build_attrs(self, base_attrs: dict, extra_attrs: dict | None = None) -> dict:
"""
Expand Down Expand Up @@ -233,7 +238,7 @@ class Media:
js = ("djangocms_link/link-widget.js",)
css = {"all": ("djangocms_link/link-widget.css",)}

def __init__(self, site_selector: bool | None = None):
def __init__(self, site_selector: bool | None = None, language: str | None = None):
if site_selector is None:
site_selector = LinkWidget.default_site_selector

Expand All @@ -242,22 +247,24 @@ def __init__(self, site_selector: bool | None = None):
for key, widget in _available_widgets.items()
if key == "always" or _mapping[key] in link_types
]
if site_selector and "internal_link" in allowed_link_types:
if "internal_link" in allowed_link_types:
index = next(
i
for i, widget in enumerate(widgets)
if widget.attrs.get("widget") == "internal_link"
)
widgets.insert(
index,
SiteAutocompleteSelect(
attrs={
"class": "js-link-site-widget",
"widget": "site",
"data-placeholder": _("Select site"),
},
),
) # Site selector
widgets[index].language = language # Pass on language to the internal link widget
if site_selector:
widgets.insert(
index,
SiteAutocompleteSelect(
attrs={
"class": "js-link-site-widget",
"widget": "site",
"data-placeholder": _("Select site"),
},
),
) # Site selector

# Remember which widget expets its content at which position
self.data_pos = {
Expand Down Expand Up @@ -287,7 +294,6 @@ def get_context(self, name: str, value: str | None, attrs: dict) -> dict:


class LinkFormField(Field):
widget = LinkWidget
external_link_validators = [
ExtendedURLValidator(allowed_link_types=allowed_link_types)
]
Expand All @@ -300,6 +306,7 @@ class LinkFormField(Field):
def __init__(self, *args, **kwargs):
kwargs.setdefault("help_text", _("Select a link type and provide a link."))
kwargs.setdefault("initial", {})
kwargs.setdefault("widget", LinkWidget(language=kwargs.pop("language", None)))
kwargs.pop("encoder", None) # Passed from LinkField's JSONField parent class
kwargs.pop("decoder", None) # but not needed
super().__init__(*args, **kwargs)
Expand Down
Binary file modified djangocms_link/locale/de/LC_MESSAGES/django.mo
Binary file not shown.
Loading