From 6302e6051e6069f20bc4a8996f239fa08e893b43 Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Thu, 22 May 2025 08:42:55 +0200 Subject: [PATCH 1/8] chore: Move from script tags containing dom elements to template tags (#8233) (#8237) * [5.1.0dev1 release process] Bumped version to 5.1.0dev1 * [5.1.0dev1 release process] compilemessages * [5.1.0dev1 release process] compiling new static files * [5.1.0dev1 release process] updating latest docs * Prepare new main branch * Fix typos * fix: No changes to changelog * fix: Remove unnecessary change to migration * Remove circular import * Fix import order * Re-introduce dummy `PlaceholderField` for legacy migrations * fix: Do not assume page url cache to be filled * fix: Structure board update sometimes failed to add all interactive elements (#8227) * fix: Scan plugin data after structure mode Xhr load * Fix test * fix js linting issues * fix: Update assets * fix: Empty plugin selectors in all but first placeholder * chore: Move from script tags containing dom elements to template tags * Update cms/models/fields.py * Update cms/models/fields.py --------- Co-authored-by: Github Release Action --- cms/plugin_rendering.py | 8 ++++---- .../frontend/unit/fixtures/plugin_child_classes.html | 4 ++-- cms/tests/test_placeholder.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cms/plugin_rendering.py b/cms/plugin_rendering.py index d2965f61931..fae1bd780e4 100644 --- a/cms/plugin_rendering.py +++ b/cms/plugin_rendering.py @@ -764,9 +764,9 @@ def _preload_placeholders_for_page(self, page, slots=None, inherit=False): class StructureRenderer(BaseRenderer): load_structure = True placeholder_edit_template = """ - + {plugin_js}{placeholder_js} """ @@ -853,9 +853,9 @@ class LegacyRenderer(ContentRenderer): placeholder_edit_template = """ {content}
- + {plugin_js}{placeholder_js} """ diff --git a/cms/tests/frontend/unit/fixtures/plugin_child_classes.html b/cms/tests/frontend/unit/fixtures/plugin_child_classes.html index 3c97d9015cf..64e5c2de1f5 100644 --- a/cms/tests/frontend/unit/fixtures/plugin_child_classes.html +++ b/cms/tests/frontend/unit/fixtures/plugin_child_classes.html @@ -1,4 +1,4 @@ - + diff --git a/cms/tests/test_placeholder.py b/cms/tests/test_placeholder.py index 33764ba4110..c0c14de0199 100644 --- a/cms/tests/test_placeholder.py +++ b/cms/tests/test_placeholder.py @@ -1023,7 +1023,7 @@ def test_sets_source_when_external_object_is_rendered(self): for placeholder in declared_placeholders: self.assertContains( response, - '""" - def get_context(self, name, value, attrs): self._build_widgets() context = super().get_context(name, value, attrs) - context['widget']['script_init'] = self._build_script(name, value, context['widget']['attrs']) + context['widget']['script_data'] = {"name": name} return context def format_output(self, rendered_widgets): @@ -122,28 +111,17 @@ def get_ajax_url(self, ajax_view): 'You should provide an ajax_view argument that can be reversed to the PageSmartLinkWidget' ) - def _build_script(self, name, value, attrs={}): - return r"""""".format( - element_id=attrs.get('id', ''), - placeholder_text=attrs.get('placeholder_text', ''), - language_code=self.language, - ajax_url=force_str(self.ajax_url) - ) + def _build_script_data(self, name, value, attrs): + return { + "id": attrs.get('id', ''), + "text": str(attrs.get('placeholder_text', '')), + "lang": self.language, + "url": force_str(self.ajax_url), + } def get_context(self, name, value, attrs): context = super().get_context(name, value, attrs) - context['widget']['script_init'] = self._build_script(name, value, context['widget']['attrs']) + context['widget']['script_data'] = self._build_script_data(name, value, context['widget']['attrs']) return context @@ -220,10 +198,10 @@ class ApplicationConfigSelect(Select): Special widget -populate by javascript- that shows application configurations depending on selected Apphooks. - Required data are injected in the page as javascript data that cms.app_hook_select.js + Required data are injected in the page as JSON data that forms.apphookselect.js uses to create the appropriate data structure. - A stub 'add-another' link is created and filled in with the correct URL by the same + A stub 'addlink' link is created and filled in with the correct URL by the same javascript. """ template_name = 'cms/widgets/applicationconfigselect.html' @@ -237,29 +215,23 @@ def __init__(self, attrs=None, choices=(), app_configs={}): self.app_configs = app_configs super().__init__(attrs, choices) - def _build_script(self, name, value, attrs={}): - configs = [] - urls = [] - for application, cms_app in self.app_configs.items(): - configs.append("'{}': [{}]".format(application, ",".join( - ["['{}', '{}']".format(config.pk, escapejs(escape(config))) for config in cms_app.get_configs()]))) # noqa - for application, cms_app in self.app_configs.items(): - urls.append(f"'{application}': '{cms_app.get_config_add_url()}'") - return r"""""".format( - apphooks_configurations=','.join(configs), - apphooks_url=','.join(urls), - apphooks_value=value, - ) + def _build_script_data(self, name, value, attrs): + configs = { + str(application): [[str(config.pk), str(config)] for config in cms_app.get_configs()] + for application, cms_app in self.app_configs.items() + } + urls = { + str(application): cms_app.get_config_add_url() + for application, cms_app in self.app_configs.items() + } + + return { + "apphooks_configuration": configs, + "apphooks_configuration_url": urls, + "apphooks_configuration_value": value, + } def get_context(self, name, value, attrs): context = super().get_context(name, value, attrs) - context['widget']['script_init'] = self._build_script(name, value, context['widget']['attrs']) + context['widget']['script_data'] = self._build_script_data(name, value, context['widget']['attrs']) return context diff --git a/cms/static/cms/js/modules/cms.changeform.js b/cms/static/cms/js/modules/cms.changeform.js index a4b1913bb14..0371419cf22 100644 --- a/cms/static/cms/js/modules/cms.changeform.js +++ b/cms/static/cms/js/modules/cms.changeform.js @@ -19,6 +19,10 @@ $(function() { $(this).parent('.form-row').hide(); }); + $('#page_form_lang_tabs .language_button').on('click', function() { + CMS.API.changeLanguage(this.dataset.adminUrl); + }); + // public api for changing the language tabs // used in admin/cms/page/change_form.html window.CMS.API.changeLanguage = function(url) { diff --git a/cms/static/cms/js/widgets/forms.apphookselect.js b/cms/static/cms/js/widgets/forms.apphookselect.js index 6ab08446fbe..e2d7c1278ba 100644 --- a/cms/static/cms/js/widgets/forms.apphookselect.js +++ b/cms/static/cms/js/widgets/forms.apphookselect.js @@ -11,7 +11,18 @@ __webpack_public_path__ = require('../modules/get-dist-path')('bundle.forms.apph // APP HOOK SELECT require.ensure([], function (require) { var $ = require('jquery'); - var apphooks_configuration = window.apphooks_configuration || {}; + var apphookData = { + apphooks_configuration: {}, + apphooks_configuration_value: undefined, + apphooks_configuration_url: {} + }; + var dataElement = document.querySelector('script[data-cms-widget-applicationconfigselect]'); + + if (dataElement) { + apphookData = JSON.parse(dataElement.querySelector('script').textContent); + } + + var apphooks_configuration = apphookData.apphooks_configuration || {}; // shorthand for jQuery(document).ready(); $(function () { @@ -33,7 +44,7 @@ require.ensure([], function (require) { for (var i = 0; i < apphooks_configuration[opt.val()].length; i++) { var selectedCfgs = ''; - if (apphooks_configuration[opt.val()][i][0] === window.apphooks_configuration_value) { + if (apphooks_configuration[opt.val()][i][0] === apphookData.apphooks_configuration_value) { selectedCfgs = 'selected="selected"'; } appCfgs.append( @@ -42,11 +53,14 @@ require.ensure([], function (require) { '' ); } - appCfgsAdd.attr('href', window.apphooks_configuration_url[opt.val()] + + appCfgsAdd.attr('href', apphookData.apphooks_configuration_url[opt.val()] + // Here we check if we are on django>=1.8 by checking if the method introduced in that version // exists, and if it does - we add `_popup` ourselves, because otherwise the popup with // apphook creation form will not be dismissed correctly (window.showRelatedObjectPopup ? '?_popup=1' : '')); + appCfgsAdd.on('click', function () { + window.showAddAnotherPopup(this); + }); appCfgsRow.removeClass('hidden'); appNsRow.addClass('hidden'); } else { diff --git a/cms/static/cms/js/widgets/forms.pageselectwidget.js b/cms/static/cms/js/widgets/forms.pageselectwidget.js index 33ea3c20b60..5891e285682 100644 --- a/cms/static/cms/js/widgets/forms.pageselectwidget.js +++ b/cms/static/cms/js/widgets/forms.pageselectwidget.js @@ -82,11 +82,11 @@ require.ensure( // init $(function() { - if (window.CMS.Widgets !== undefined && window.CMS.Widgets._pageSelectWidgets !== undefined) { - window.CMS.Widgets._pageSelectWidgets.forEach(function (widget) { - new PageSelectWidget(widget); - }); - } + document.querySelectorAll('[data-cms-widget-pageselect]').forEach(function (el) { + var widget = JSON.parse(el.querySelector('script').textContent); + + new PageSelectWidget(widget); + }); }); }, 'admin.widget' diff --git a/cms/static/cms/js/widgets/forms.pagesmartlinkwidget.js b/cms/static/cms/js/widgets/forms.pagesmartlinkwidget.js index 0c184c32d4a..932740e4f50 100644 --- a/cms/static/cms/js/widgets/forms.pagesmartlinkwidget.js +++ b/cms/static/cms/js/widgets/forms.pagesmartlinkwidget.js @@ -95,10 +95,10 @@ require.ensure([], function (require) { window.CMS.PageSmartLinkWidget = PageSmartLinkWidget; $(function () { - if (window.CMS.Widgets !== undefined && window.CMS.Widgets._pageSmartLinkWidgets !== undefined) { - window.CMS.Widgets._pageSmartLinkWidgets.forEach(function (widget) { - new PageSmartLinkWidget(widget); - }); - } + document.querySelectorAll('[data-cms-widget-pagesmartlinkwidget]').forEach(function (el) { + var widget = JSON.parse(el.querySelector('script').textContent); + + new PageSmartLinkWidget(widget); + }); }); }, 'admin.widget'); diff --git a/cms/templates/admin/cms/page/change_form.html b/cms/templates/admin/cms/page/change_form.html index 6de5ea819cc..6a231404b5b 100644 --- a/cms/templates/admin/cms/page/change_form.html +++ b/cms/templates/admin/cms/page/change_form.html @@ -43,7 +43,7 @@ {% if show_language_tabs and not show_permissions %}
{% for lang_code, lang_name in language_tabs %} - {% endfor %} diff --git a/cms/templates/cms/widgets/applicationconfigselect.html b/cms/templates/cms/widgets/applicationconfigselect.html index a0874493954..28ec95ebba9 100644 --- a/cms/templates/cms/widgets/applicationconfigselect.html +++ b/cms/templates/cms/widgets/applicationconfigselect.html @@ -1,4 +1,6 @@ -{% load i18n static %} +{% load i18n %} {% include 'django/forms/widgets/select.html' %} -{{ widget.script_init|safe }} - + + diff --git a/cms/templates/cms/widgets/pageselectwidget.html b/cms/templates/cms/widgets/pageselectwidget.html index db16509a92c..3cbd3c2dcdc 100644 --- a/cms/templates/cms/widgets/pageselectwidget.html +++ b/cms/templates/cms/widgets/pageselectwidget.html @@ -1,2 +1,4 @@ {% include 'django/forms/widgets/multiwidget.html' %} -{{ widget.script_init|safe }} + diff --git a/cms/templates/cms/widgets/pagesmartlinkwidget.html b/cms/templates/cms/widgets/pagesmartlinkwidget.html index 81514a118d8..411e438ed5e 100644 --- a/cms/templates/cms/widgets/pagesmartlinkwidget.html +++ b/cms/templates/cms/widgets/pagesmartlinkwidget.html @@ -1,2 +1,4 @@ {% include 'django/forms/widgets/text.html' %} -{{ widget.script_init|safe }} + diff --git a/cms/tests/test_forms.py b/cms/tests/test_forms.py index 251a3da1a99..7581effea38 100644 --- a/cms/tests/test_forms.py +++ b/cms/tests/test_forms.py @@ -232,12 +232,7 @@ def get_config_add_url(self): app_config_select = ApplicationConfigSelect(app_configs=app_configs) output = app_config_select.render("application_configurations", 1) self.assertFalse('' in output) - self.assertTrue( - "\\u0026lt\\u003Bscript\\u0026gt\\u003Balert(" - "\\u0026quot\\u003Bbad\\u002Dstuff\\u0026quot" - "\\u003B)\\u003B\\u0026lt\\u003B/script\\u0026gt" - "\\u003B" in output - ) + self.assertTrue('\\u003Cscript\\u003Ealert(\\"bad-stuff\\");\\u003C/script\\u003E' in output) def test_move_page_form(self): """Test the MovePageForm validation and behavior""" From 6327e0f9663da417d7802755da314909ad8dfbdd Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Wed, 18 Jun 2025 13:12:34 +0200 Subject: [PATCH 7/8] chore: Add hash to `FuzzyInt` test class to satisfy ruff (#8259) --- cms/test_utils/util/fuzzy_int.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cms/test_utils/util/fuzzy_int.py b/cms/test_utils/util/fuzzy_int.py index 781dc1b7dcb..ed2b58932af 100644 --- a/cms/test_utils/util/fuzzy_int.py +++ b/cms/test_utils/util/fuzzy_int.py @@ -1,5 +1,3 @@ - - class FuzzyInt(int): def __new__(cls, lowest, highest): obj = super().__new__(cls, highest) @@ -12,3 +10,7 @@ def __eq__(self, other): def __repr__(self): return "[%d..%d]" % (self.lowest, self.highest) + + def __hash__(self): + # Combine the hash of the lowest and highest attributes, ensuring hash consistency. + return hash((self.lowest, self.highest)) From ec2f4e8155f69d44da2bab67482391eea97fdc04 Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Tue, 24 Jun 2025 22:12:13 +0200 Subject: [PATCH 8/8] fix: Apphook widget detection (#8263) * fix: Apphook widget detection * Fix: Add related --- .../cms/js/widgets/forms.apphookselect.js | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/cms/static/cms/js/widgets/forms.apphookselect.js b/cms/static/cms/js/widgets/forms.apphookselect.js index e2d7c1278ba..38a2a9e2c90 100644 --- a/cms/static/cms/js/widgets/forms.apphookselect.js +++ b/cms/static/cms/js/widgets/forms.apphookselect.js @@ -10,39 +10,40 @@ __webpack_public_path__ = require('../modules/get-dist-path')('bundle.forms.apph // ############################################################################# // APP HOOK SELECT require.ensure([], function (require) { - var $ = require('jquery'); - var apphookData = { + const $ = require('jquery'); + let apphookData = { apphooks_configuration: {}, apphooks_configuration_value: undefined, apphooks_configuration_url: {} }; - var dataElement = document.querySelector('script[data-cms-widget-applicationconfigselect]'); - - if (dataElement) { - apphookData = JSON.parse(dataElement.querySelector('script').textContent); - } - - var apphooks_configuration = apphookData.apphooks_configuration || {}; // shorthand for jQuery(document).ready(); $(function () { - var appHooks = $('#application_urls, #id_application_urls'); - var selected = appHooks.find('option:selected'); - var appNsRow = $('.form-row.application_namespace, .form-row.field-application_namespace'); - var appNs = appNsRow.find('#application_namespace, #id_application_namespace'); - var appCfgsRow = $('.form-row.application_configs, .form-row.field-application_configs'); - var appCfgs = appCfgsRow.find('#application_configs, #id_application_configs'); - var appCfgsAdd = appCfgsRow.find('#add_application_configs'); - var original_ns = appNs.val(); + const dataElement = document.querySelector('div[data-cms-widget-applicationconfigselect]'); + + if (dataElement) { + apphookData = JSON.parse(dataElement.querySelector('script').textContent); + } + + const apphooks_configuration = apphookData.apphooks_configuration || {}; + + const appHooks = $('#application_urls, #id_application_urls'); + const selected = appHooks.find('option:selected'); + const appNsRow = $('.form-row.application_namespace, .form-row.field-application_namespace'); + const appNs = appNsRow.find('#application_namespace, #id_application_namespace'); + const appCfgsRow = $('.form-row.application_configs, .form-row.field-application_configs'); + const appCfgs = appCfgsRow.find('#application_configs, #id_application_configs'); + const appCfgsAdd = appCfgsRow.find('#add_application_configs'); + const original_ns = appNs.val(); // Shows / hides namespace / config selection widgets depending on the user input appHooks.setupNamespaces = function () { - var opt = $(this).find('option:selected'); + const opt = $(this).find('option:selected'); if ($(appCfgs).length > 0 && apphooks_configuration[opt.val()]) { appCfgs.html(''); - for (var i = 0; i < apphooks_configuration[opt.val()].length; i++) { - var selectedCfgs = ''; + for (let i = 0; i < apphooks_configuration[opt.val()].length; i++) { + let selectedCfgs = ''; if (apphooks_configuration[opt.val()][i][0] === apphookData.apphooks_configuration_value) { selectedCfgs = 'selected="selected"'; @@ -58,7 +59,8 @@ require.ensure([], function (require) { // exists, and if it does - we add `_popup` ourselves, because otherwise the popup with // apphook creation form will not be dismissed correctly (window.showRelatedObjectPopup ? '?_popup=1' : '')); - appCfgsAdd.on('click', function () { + appCfgsAdd.on('click', function (ev) { + ev.preventDefault(); window.showAddAnotherPopup(this); }); appCfgsRow.removeClass('hidden');