Skip to content

Commit

Permalink
Add two step select2 internal link widget
Browse files Browse the repository at this point in the history
  • Loading branch information
fsbraun committed Oct 25, 2024
1 parent c8882ed commit efc6fd1
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 51 deletions.
8 changes: 4 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ You can run tests by executing::
.. |djangocms| image:: https://img.shields.io/badge/django%20CMS-3.7%2B-blue.svg
:target: https://www.django-cms.org/

Updating from version 4 or lower
Upgrading from version 4 or lower
--------------------------------

django CMS Link 5 is a rewrite of the plugin. If you are updating from
Expand All @@ -172,6 +172,6 @@ fields.
.. warning::

Migration has worked for some people seamlessly. We strongly recommend to
backup your database before updating to version 5. If you encounter any
issues, please report them on
`GitHub <https://github.com/django-cms/djangocms-link/issues>`_.
backup your database before updating to version 5. If you encounter any
issues, please report them on
`GitHub <https://github.com/django-cms/djangocms-link/issues>`_.
24 changes: 19 additions & 5 deletions djangocms_link/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def get(self, request, *args, **kwargs):
# Get name of a reference
return self.get_reference(request)

self.term, self.language = self.process_request(request)
self.term, self.language, self.site = self.process_request(request)

if not self.has_perm(request):
raise PermissionDenied
Expand Down Expand Up @@ -87,23 +87,32 @@ def get_queryset(self):
if _version >= 4:
try:
# django CMS 4.2+
qs = list(
qs = (
PageContent.admin_manager.filter(language=self.language, title__icontains=self.term)
.current_content()
.order_by("page__path")
)
if self.site:
qs = qs.filter(page__site_id=self.site)
qs = list(qs)
except FieldError:
# django CMS 4.0 - 4.1
qs = list(
qs = (
PageContent.admin_manager.filter(language=self.language, title__icontains=self.term)
.current_content()
.order_by("page__node__path")
)
if self.site:
qs = qs.filter(page__site_id=self.site)
qs = list(qs)
else:
# django CMS 3
qs = list(PageContent.objects.filter(
qs = (PageContent.objects.filter(
language=self.language, title__icontains=self.term
).order_by("page__node__path"))
if self.site:
qs = qs.filter(page__node_site_id=self.site)
qs = list(qs)
for page_content in qs:
# Patch the missing get_absolute_url method
page_content.get_absolute_url = lambda: page_content.page.get_absolute_url()
Expand All @@ -114,8 +123,13 @@ def process_request(self, request):
Validate request integrity, extract and return request parameters.
"""
term = request.GET.get("term", "").strip("  ").lower()
site = request.GET.get("app_label", "") # Django admin's app_label is abused as site id
try:
site = int(site)
except ValueError:
site = None
language = get_language_from_request(request)
return term, language
return term, language, site

def has_perm(self, request, obj=None):
"""Check if user has permission to access the related model."""
Expand Down
63 changes: 58 additions & 5 deletions djangocms_link/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
from django.conf import settings
from django.contrib.admin import site
from django.contrib.admin.widgets import SELECT2_TRANSLATIONS, AutocompleteSelect
from django.db.models import JSONField, ManyToOneRel
from django.forms import Field, MultiWidget, Select, TextInput, URLInput
from django.contrib.sites.models import Site
from django.db.models import JSONField, ManyToOneRel, ForeignKey, SET_NULL

Check failure on line 8 in djangocms_link/fields.py

View workflow job for this annotation

GitHub Actions / flake8

'django.db.models.ForeignKey' imported but unused

Check failure on line 8 in djangocms_link/fields.py

View workflow job for this annotation

GitHub Actions / flake8

'django.db.models.SET_NULL' imported but unused

Check failure on line 8 in djangocms_link/fields.py

View workflow job for this annotation

GitHub Actions / flake8

'django.db.models.ForeignKey' imported but unused

Check failure on line 8 in djangocms_link/fields.py

View workflow job for this annotation

GitHub Actions / flake8

'django.db.models.SET_NULL' imported but unused
from django.forms import Field, MultiWidget, Select, TextInput, URLInput, CheckboxInput

Check failure on line 9 in djangocms_link/fields.py

View workflow job for this annotation

GitHub Actions / flake8

'django.forms.CheckboxInput' imported but unused

Check failure on line 9 in djangocms_link/fields.py

View workflow job for this annotation

GitHub Actions / flake8

'django.forms.CheckboxInput' imported but unused
from django.utils.translation import get_language
from django.utils.translation import gettext_lazy as _

Expand Down Expand Up @@ -35,6 +36,9 @@


class LinkAutoCompleteWidget(AutocompleteSelect):
def __init__(self, attrs=None):
super().__init__(None, None, attrs)

def get_internal_obj(self, values):
internal_obj = []
for value in values:
Expand Down Expand Up @@ -105,15 +109,57 @@ def build_attrs(self, base_attrs, extra_attrs=None):
return attrs


class SiteAutocompleteSelect(AutocompleteSelect):
no_sites = None

def __init__(self, attrs=None):
try:
from cms.models.pagemodel import TreeNode

field = TreeNode._meta.get_field("site")
except ImportError:
from cms.models import Page

field = Page._meta.get_field("site")
super().__init__(field, site, attrs)

def optgroups(self, name, value, attr=None):
default = (None, [], 0)
groups = [default]
has_selected = False
selected_choices = set(value)
default[1].append(self.create_option(name, "", "", False, 0))

site = Site.objects.get_current()
option_value, option_label = site.pk, str(site)

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
)
)
return groups


class LinkWidget(MultiWidget):
template_name = "djangocms_link/admin/link_widget.html"
data_pos = {}

number_sites = None
class Media:

Check failure on line 154 in djangocms_link/fields.py

View workflow job for this annotation

GitHub Actions / flake8

expected 1 blank line, found 0

Check failure on line 154 in djangocms_link/fields.py

View workflow job for this annotation

GitHub Actions / flake8

expected 1 blank line, found 0
js = ("djangocms_link/link-widget.js",)
css = {"all": ("djangocms_link/link-widget.css",)}

def __init__(self):
# Get the number of sites only once
if LinkWidget.number_sites is None:
LinkWidget.number_sites = Site.objects.count()

widgets = [
Select(
choices=list(link_types.items()),
Expand All @@ -122,14 +168,20 @@ def __init__(self):
"data-help": _("No destination selected. Use the dropdown to select a destination.")
},
), # Link type selector
SiteAutocompleteSelect(
attrs={
"class": "js-link-site-widget",
"widget": "site",
"data-placeholder": "XXX", #_("Select site"),

Check failure on line 175 in djangocms_link/fields.py

View workflow job for this annotation

GitHub Actions / flake8

at least two spaces before inline comment

Check failure on line 175 in djangocms_link/fields.py

View workflow job for this annotation

GitHub Actions / flake8

inline comment should start with '# '

Check failure on line 175 in djangocms_link/fields.py

View workflow job for this annotation

GitHub Actions / flake8

at least two spaces before inline comment

Check failure on line 175 in djangocms_link/fields.py

View workflow job for this annotation

GitHub Actions / flake8

inline comment should start with '# '
},
), # Site selector
LinkAutoCompleteWidget(
field=None,
admin_site=None,
attrs={
"widget": "internal_link",
"data-help": _(
"Select from available internal destinations. Optionally, add an anchor to scroll to."
),
"data-placeholder": _("Select internal destination"),
},
), # Internal link selector
URLInput(
Expand Down Expand Up @@ -161,6 +213,7 @@ def __init__(self):
},
),
)

# Remember which widget expets its content at which position
self.data_pos = {
widget.attrs.get("widget"): i for i, widget in enumerate(widgets)
Expand Down
75 changes: 46 additions & 29 deletions djangocms_link/static/djangocms_link/link-widget.css
Original file line number Diff line number Diff line change
@@ -1,58 +1,75 @@
.link-widget {
width: 100%;
display: flex;
display: block;
margin-bottom: 0.5em;
.link-type-selector {
margin-inline-end: 1em;
display: block;
flex-shrink: 1;
display: inline-block;
width: calc(25% - 1em);
flex-shrink: 2;
select {
width: 100%;
min-width: unset;
}
}
.external_link, .internal_link, .file_link, .anchor {
.external_link, .internal_link, .file_link, .anchor, .site {
display: none;
width: 100%;
margin-inline-end: 1em;
padding: 0;
select, input {
width: 100%;
}
span.select2 {
display: block;
display: inline-block;
width: 100% !important;
}
}
.external_link {
width: 75%;
}
.internal_link {
width: calc(60% - 1em);
margin-inline-end: 1em;
}
.anchor {
width: 15%; /* end of line, no 1em margin to remove */
}
.file_link {
margin-top: 0.5em;
}
&[data-type="external_link"] .external_link,
&[data-type="internal_link"] .internal_link,
&[data-type="internal_link"] .anchor,
&[data-type="file_link"], &[data-type="file_link"] .file_link
&[data-type="internal_link"] .site,
&[data-type="internal_link"] .anchor
{
display: block
display: inline-block;
}
&[data-type="file_link"] .file_link {
display: block;
width: 100%;
}
&[data-type="file_link"] .link-type-selector,
&[data-type="empty"] .link-type-selector{
margin-inline-end: 0;
width: 33%;
}
.link-settings-menu {
margin-inline-start: 1em;
float: right;
position: relative;
border-width: 1px;
border-style: solid;
border-radius: 3px;
border-color: var(--dca-gray-light, var(--border-color));
padding: 1rem;
box-sizing: border-box;
height: 1px;
width: 1px;
svg {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

&:has(.site) {
/* if site subwidget is present, arrange widgets in two lines */
.site {
width: 75%;
margin-inline-end: 0;
margin-bottom: 0.5em;
}
.internal_link {
width: calc(60% - 1em);
margin-inline-start: 25%;
margin-inline-end: 1em;
}
.anchor (
margin-top: -3em;
)
)
}

.select2-container .select2-selection--single {
height: 2.55em;
}
Expand Down
16 changes: 15 additions & 1 deletion djangocms_link/static/djangocms_link/link-widget.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-env es11 */
/* jshint esversion: 11 */
/* global document */
/* global document django */

document.addEventListener('DOMContentLoaded' , () => {
'use strict';
Expand All @@ -25,4 +25,18 @@ document.addEventListener('DOMContentLoaded' , () => {
e.target.closest('.link-widget').querySelector('input[widget="anchor"]').value = '';
});
}

// If site widget changes, clear internal link widget
for (let item of document.querySelectorAll('.js-link-site-widget')) {
console.warn(item);
django.jQuery(item).on('change', e => {
const site_select2 = django.jQuery(e.target);
const internal_link_select2 = site_select2.closest('.link-widget').find('[widget="internal_link"]');
internal_link_select2.attr('data-app-label', site_select2.val());
internal_link_select2.val(null).trigger('change');
});
item.addEventListener("change", (e) => {
console.warn(e.target.closest('.link-widget').querySelector('[widget="internal_link"]'));
});
}
});
4 changes: 2 additions & 2 deletions djangocms_link/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ def __call__(self, value):
return value


class ExtendedURLValidator(URLValidator):
class ExtendedURLValidator(IntranetURLValidator):
# Phone numbers don't match the host regex in Django's validator,
# so we test for a simple alternative.
tel_re = r'^tel\:[0-9\#\*\-\.\(\)\+]+$'
tel_re = r'^tel\:[0-9 \#\*\-\.\(\)\+]+$'

def __call__(self, value):
if not isinstance(value, str) or len(value) > self.max_length:
Expand Down
26 changes: 26 additions & 0 deletions tests/test_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,29 @@ def test_file(self):
)
self.assertIn("test_file.pdf", plugin.get_link())
self.assertIn("/media/filer_public/", plugin.get_link())

def test_rendering(self):
plugin = add_plugin(

Check failure on line 181 in tests/test_plugins.py

View workflow job for this annotation

GitHub Actions / flake8

local variable 'plugin' is assigned to but never used

Check failure on line 181 in tests/test_plugins.py

View workflow job for this annotation

GitHub Actions / flake8

local variable 'plugin' is assigned to but never used
self.get_placeholders(self.page, self.language).get(slot="content"),
"LinkPlugin",
"en",
name="Link",
link={"internal_link": f"cms.page:{self.page.pk}"},
)
self.publish(self.page, self.language)

response = self.client.get(self.page.get_absolute_url(self.language))
self.assertContains(response, '<a href="/en/content/">Link</a>')

def test_rendering_fallback(self):
plugin = add_plugin(

Check failure on line 194 in tests/test_plugins.py

View workflow job for this annotation

GitHub Actions / flake8

local variable 'plugin' is assigned to but never used

Check failure on line 194 in tests/test_plugins.py

View workflow job for this annotation

GitHub Actions / flake8

local variable 'plugin' is assigned to but never used
self.get_placeholders(self.page, self.language).get(slot="content"),
"LinkPlugin",
"en",
name="Link",
link={"internal_link": f"cms.page:0"},

Check failure on line 199 in tests/test_plugins.py

View workflow job for this annotation

GitHub Actions / flake8

f-string is missing placeholders

Check failure on line 199 in tests/test_plugins.py

View workflow job for this annotation

GitHub Actions / flake8

f-string is missing placeholders
)
self.publish(self.page, self.language)

response = self.client.get(self.page.get_absolute_url(self.language))
self.assertContains(response, '<span>Link</span>')
Loading

0 comments on commit efc6fd1

Please sign in to comment.