Skip to content

Commit 2762012

Browse files
fix: Only allow turning plugins into alias if they can be a root plugin (#286)
* fix: Only allow turning plugins into alias if they can be a root plugin * Update djangocms_alias/static/djangocms_alias/js/databridge.js Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * fix tests * fix: Improve databridge.js * fix: non-breaking space replaced * feat: Allow for fast v5 screen update * fix: Some more fixes, including standard form rendering --------- Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
1 parent 3f2fb02 commit 2762012

File tree

7 files changed

+203
-70
lines changed

7 files changed

+203
-70
lines changed

djangocms_alias/cms_plugins.py

Lines changed: 102 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from cms.models import Page
22
from cms.plugin_base import CMSPluginBase, PluginMenuItem
33
from cms.plugin_pool import plugin_pool
4-
from cms.toolbar.utils import get_object_edit_url
4+
from cms.toolbar.utils import get_object_edit_url, get_plugin_toolbar_info, get_plugin_tree
55
from cms.utils import get_language_from_request
66
from cms.utils.permissions import (
77
get_model_permission_codename,
@@ -38,11 +38,36 @@ class Alias(CMSPluginBase):
3838
model = AliasPlugin
3939
form = AliasPluginForm
4040

41+
create_alias_fieldset = (
42+
(
43+
None,
44+
{
45+
"fields": (
46+
"name",
47+
"site",
48+
"category",
49+
"replace",
50+
"plugin",
51+
"placeholder",
52+
"language",
53+
),
54+
},
55+
),
56+
)
57+
58+
autocomplete_fields = ["alias"]
59+
4160
def get_render_template(self, context, instance, placeholder):
4261
if isinstance(instance.placeholder.source, AliasContent) and instance.is_recursive():
4362
return "djangocms_alias/alias_recursive.html"
4463
return f"djangocms_alias/{instance.template}/alias.html"
4564

65+
@classmethod
66+
def _get_allowed_root_plugins(cls):
67+
if not hasattr(cls, "_cached_allowed_root_plugins"):
68+
cls._cached_allowed_root_plugins = set(plugin_pool.get_all_plugins(root_plugin=True))
69+
return cls._cached_allowed_root_plugins
70+
4671
@classmethod
4772
def get_extra_plugin_menu_items(cls, request, plugin):
4873
if plugin.plugin_type == cls.__name__:
@@ -82,15 +107,19 @@ def get_extra_plugin_menu_items(cls, request, plugin):
82107
"plugin": plugin.pk,
83108
"language": get_language_from_request(request),
84109
}
85-
endpoint = add_url_parameters(admin_reverse(CREATE_ALIAS_URL_NAME), **data)
86-
return [
87-
PluginMenuItem(
88-
_("Create Alias"),
89-
endpoint,
90-
action="modal",
91-
attributes={"cms-icon": "alias"},
92-
),
93-
]
110+
# Check if the plugin can become root: Should be allowed as a root plugin (in the alias)
111+
can_become_alias = plugin.get_plugin_class() in cls._get_allowed_root_plugins()
112+
if can_become_alias:
113+
endpoint = add_url_parameters(admin_reverse(CREATE_ALIAS_URL_NAME), **data)
114+
return [
115+
PluginMenuItem(
116+
_("Create Alias"),
117+
endpoint,
118+
action="modal",
119+
attributes={"cms-icon": "alias"},
120+
),
121+
]
122+
return []
94123

95124
@classmethod
96125
def get_extra_placeholder_menu_items(cls, request, placeholder):
@@ -150,6 +179,7 @@ def detach_alias_plugin(cls, plugin, language):
150179
source_plugins = plugin.alias.get_plugins(language, show_draft_content=True) # We're in edit mode
151180
target_placeholder = plugin.placeholder
152181
plugin_position = plugin.position
182+
plugin_parent = plugin.parent
153183
target_placeholder.delete_plugin(plugin)
154184
if source_plugins:
155185
if target_last_plugin := target_placeholder.get_last_plugin(plugin.language):
@@ -163,6 +193,7 @@ def detach_alias_plugin(cls, plugin, language):
163193
source_plugins,
164194
placeholder=target_placeholder,
165195
language=language,
196+
root_plugin=plugin_parent,
166197
start_positions={language: plugin_position},
167198
)
168199
return []
@@ -215,17 +246,28 @@ def create_alias_view(self, request):
215246
)
216247

217248
if not create_form.is_valid():
218-
opts = self.model._meta
249+
from django.contrib import admin
250+
251+
fieldsets = self.create_alias_fieldset
252+
admin_form = admin.helpers.AdminForm(create_form, fieldsets, {})
253+
self.opts = self.model._meta
254+
self.admin_site = admin.site
219255
context = {
220-
"form": create_form,
221-
"has_change_permission": True,
222-
"opts": opts,
223-
"root_path": admin_reverse("index"),
256+
"title": _("Create Alias"),
257+
"adminform": admin_form,
224258
"is_popup": True,
225-
"app_label": opts.app_label,
226-
"media": (Alias().media + create_form.media),
259+
"media": admin_form.media,
260+
"errors": create_form.errors,
261+
"preserved_filters": self.get_preserved_filters(request),
262+
"inline_admin_formsets": [],
227263
}
228-
return TemplateResponse(request, "djangocms_alias/create_alias.html", context)
264+
return self.render_change_form(
265+
request,
266+
context,
267+
add=True,
268+
change=False,
269+
obj=None,
270+
)
229271

230272
plugins = create_form.get_plugins()
231273

@@ -242,10 +284,13 @@ def create_alias_view(self, request):
242284
emit_content_change([alias_content])
243285

244286
if replace:
245-
return self.render_close_frame(
287+
plugin = create_form.cleaned_data.get("plugin")
288+
placeholder = create_form.cleaned_data.get("placeholder")
289+
return self.render_replace_response(
246290
request,
247-
obj=alias_plugin,
248-
action="reload",
291+
new_plugins=[alias_plugin],
292+
source_placeholder=placeholder,
293+
source_plugin=plugin,
249294
)
250295
return TemplateResponse(request, "admin/cms/page/close_frame.html")
251296

@@ -258,6 +303,7 @@ def detach_alias_plugin_view(self, request, plugin_pk):
258303
if request.method == "GET":
259304
opts = self.model._meta
260305
context = {
306+
"title": _("Detach Alias"),
261307
"has_change_permission": True,
262308
"opts": opts,
263309
"root_path": admin_reverse("index"),
@@ -276,17 +322,48 @@ def detach_alias_plugin_view(self, request, plugin_pk):
276322
if not can_detach:
277323
raise PermissionDenied
278324

279-
self.detach_alias_plugin(
325+
copied_plugins = self.detach_alias_plugin(
280326
plugin=instance,
281327
language=language,
282328
)
283329

284-
return self.render_close_frame(
330+
return self.render_replace_response(
285331
request,
286-
obj=instance,
287-
action="reload",
332+
new_plugins=copied_plugins,
333+
source_plugin=instance,
288334
)
289335

336+
def render_replace_response(self, request, new_plugins, source_placeholder=None, source_plugin=None):
337+
move_plugins, add_plugins = [], []
338+
for plugin in new_plugins:
339+
root = plugin.parent.get_bound_plugin() if plugin.parent else plugin
340+
341+
plugins = [root] + list(root.get_descendants())
342+
343+
plugin_order = plugin.placeholder.get_plugin_tree_order(
344+
plugin.language,
345+
parent_id=plugin.parent_id,
346+
)
347+
plugin_tree = get_plugin_tree(request, plugins, target_plugin=root)
348+
move_data = get_plugin_toolbar_info(plugin)
349+
move_data["plugin_order"] = plugin_order
350+
move_data.update(plugin_tree)
351+
move_plugins.append(move_data)
352+
add_plugins.append({**get_plugin_toolbar_info(plugin), "structure": plugin_tree})
353+
data = {
354+
"addedPlugins": add_plugins,
355+
"movedPlugins": move_plugins,
356+
"is_popup": True,
357+
}
358+
if source_plugin and source_plugin.pk:
359+
data["replacedPlugin"] = get_plugin_toolbar_info(source_plugin)
360+
if source_placeholder and source_placeholder.pk:
361+
data["replacedPlaceholder"] = {
362+
"placeholder_id": source_placeholder.pk,
363+
"deleted": True,
364+
}
365+
return self.render_close_frame(request, obj=None, action="ALIAS_REPLACE", extra_data=data)
366+
290367
def alias_usage_view(self, request, pk):
291368
if not request.user.is_staff:
292369
raise PermissionDenied

djangocms_alias/forms.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from django.contrib import admin
1010
from django.contrib.admin.widgets import (
1111
AdminTextInputWidget,
12+
AutocompleteSelect,
1213
RelatedFieldWidgetWrapper,
1314
)
1415
from django.contrib.sites.models import Site
@@ -68,6 +69,9 @@ class BaseCreateAliasForm(forms.Form):
6869
)
6970
language = forms.CharField(widget=forms.HiddenInput())
7071

72+
class Media:
73+
js = ("djangocms_alias/js/databridge.js",)
74+
7175
def clean(self):
7276
cleaned_data = super().clean()
7377

@@ -104,14 +108,37 @@ class CreateAliasForm(BaseCreateAliasForm):
104108
required=False,
105109
)
106110

107-
def __init__(self, *args, **kwargs):
111+
def __init__(self, *args, initial=None, **kwargs):
108112
self.user = kwargs.pop("user")
109113

110-
super().__init__(*args, **kwargs)
114+
super().__init__(*args, initial=initial, **kwargs)
111115

116+
self.fields["site"].widget = AutocompleteSelect(
117+
Alias.site.field,
118+
admin.site,
119+
choices=self.fields["site"].choices,
120+
attrs={"data-placeholder": _("Select a site")},
121+
)
122+
self.fields["category"].widget = AutocompleteSelect(
123+
Alias.category.field,
124+
admin.site,
125+
choices=self.fields["category"].choices,
126+
attrs={"data-placeholder": _("Select a category")},
127+
)
128+
129+
# Remove the replace option, if user does not have permission to add "Alias"
112130
if not has_plugin_permission(self.user, "Alias", "add"):
113131
self.fields["replace"].widget = forms.HiddenInput()
114132

133+
# Remove the replace option, if "Alias" cannot be a child of parent plugin
134+
initial = initial or {}
135+
plugin = initial.get("plugin")
136+
if plugin and plugin.parent:
137+
plugin_class = plugin.parent.get_plugin_class()
138+
allowed_children = plugin_class.get_child_classes(plugin.placeholder.slot, instance=plugin.parent)
139+
if allowed_children and "Alias" not in allowed_children:
140+
self.fields["replace"].widget = forms.HiddenInput()
141+
115142
self.set_category_widget(self.user)
116143
self.fields["site"].initial = get_current_site()
117144

@@ -217,7 +244,7 @@ class Meta:
217244
class Select2Mixin:
218245
class Media:
219246
css = {
220-
"all": ("cms/js/select2/select2.css",),
247+
"screen": ("cms/js/select2/select2.css",),
221248
}
222249
js = (
223250
"admin/js/jquery.init.js",

djangocms_alias/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ class Category(TranslatableModel):
5757
class Meta:
5858
verbose_name = _("category")
5959
verbose_name_plural = _("categories")
60+
ordering = ["translations__name"]
6061

6162
def __str__(self):
6263
# Be sure to be able to see the category name even if it's not in the current language
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
(function () {
2+
3+
const processDataBridge = function (data) {
4+
let actionsPerformed = 0;
5+
let updateNeeded = false;
6+
7+
if (data.replacedPlaceholder) {
8+
updateNeeded |= CMS.API.StructureBoard.handleClearPlaceholder(data.replacedPlaceholder);
9+
actionsPerformed++;
10+
}
11+
if (data.replacedPlugin) {
12+
updateNeeded |= CMS.API.StructureBoard.handleDeletePlugin(data.replacedPlugin);
13+
actionsPerformed++;
14+
}
15+
if (data.addedPlugins) {
16+
for (const addedPlugin of data.addedPlugins) {
17+
updateNeeded |= CMS.API.StructureBoard.handleAddPlugin(addedPlugin);
18+
actionsPerformed++;
19+
}
20+
}
21+
if (data.movedPlugins) {
22+
for (const movedPlugin of data.movedPlugins) {
23+
updateNeeded |= CMS.API.StructureBoard.handleMovePlugin(movedPlugin);
24+
actionsPerformed++;
25+
}
26+
}
27+
28+
if (updateNeeded) {
29+
CMS.API.StructureBoard._requestcontent = null;
30+
CMS.API.StructureBoard.updateContent();
31+
}
32+
return actionsPerformed;
33+
}
34+
35+
const iframe = window.parent.document.querySelector('.cms-modal-frame > iframe');
36+
const {CMS} = window.parent;
37+
38+
if (!iframe || !CMS) {
39+
return;
40+
}
41+
42+
// Register the event handler in the capture phase to increase the chance it runs first
43+
iframe.addEventListener('load', function (event) {
44+
const iframeDocument = iframe.contentDocument || iframe.contentWindow.document;
45+
const dataBridge = iframeDocument.body.querySelector('script#data-bridge');
46+
if (dataBridge) {
47+
try {
48+
const data = JSON.parse(dataBridge.textContent);
49+
if (data.action === 'ALIAS_REPLACE') {
50+
event.stopPropagation();
51+
dataBridge.parentNode.removeChild(dataBridge);
52+
if (processDataBridge(data)) {
53+
}
54+
iframe.dispatchEvent(new Event('load')); // re-dispatch load event to trigger modal close
55+
}
56+
} catch (error) {
57+
window.parent.console.error('Error parsing data bridge script:', error);
58+
}
59+
iframeDocument.body.textContent = JSON.stringify(dataBridge);
60+
}
61+
}, true); // 'true' sets the event to be handled in the capture phase before the modals handler
62+
})();

djangocms_alias/templates/djangocms_alias/create_alias.html

Lines changed: 0 additions & 34 deletions
This file was deleted.

djangocms_alias/templates/djangocms_alias/detach_alias.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
{% extends "admin/delete_confirmation.html" %}
2-
{% load i18n admin_urls cms_tags %}
3-
2+
{% load i18n admin_urls cms_tags static %}
43
{% block breadcrumbs %}{% endblock %}
54

65
{% block content %}
@@ -14,7 +13,8 @@
1413
<input type="hidden" name="post" value="yes" />
1514
<div class="submit-row">
1615
<a href="#" class="button cancel-link">{% trans "No, take me back" %}</a>
17-
<input type="submit" class="deletelink" value="{% trans "Yes, I'm sure" %}" />
16+
<input type="submit" class="deletelink" value="{% translate "Yes, I'm sure" %}" />
1817
</div>
1918
</form>
19+
<script src="{% static 'djangocms_alias/js/databridge.js' %}"></script>
2020
{% endblock %}

0 commit comments

Comments
 (0)