diff --git a/cms/apps.py b/cms/apps.py index c9491bcc782..1746d3e4137 100644 --- a/cms/apps.py +++ b/cms/apps.py @@ -5,6 +5,7 @@ class CMSConfig(AppConfig): name = 'cms' verbose_name = _("django CMS") + default_auto_field = 'django.db.models.AutoField' def ready(self): from cms.utils.setup import setup, setup_cms_apps diff --git a/cms/forms/widgets.py b/cms/forms/widgets.py index 9e39b5448c9..c94ae13ecf4 100644 --- a/cms/forms/widgets.py +++ b/cms/forms/widgets.py @@ -74,21 +74,10 @@ def _build_widgets(self): Select(choices=self.choices, attrs={'style': "display:none;"}), ) - def _build_script(self, name, value, attrs={}): - return rf"""""" - 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/migrations/0027_title_placeholders_data_migration.py b/cms/migrations/0027_title_placeholders_data_migration.py index 82869c7b139..a5eec8ddabe 100644 --- a/cms/migrations/0027_title_placeholders_data_migration.py +++ b/cms/migrations/0027_title_placeholders_data_migration.py @@ -95,6 +95,7 @@ def forwards(apps, schema_editor): .using(db_alias) .filter(pk__in=old_placeholder_ids) .annotate(plugin_count=models.Count('cmsplugin')) + .only("pk") ) if old_placeholders.filter(plugin_count__gt=0).exists(): diff --git a/cms/models/contentmodels.py b/cms/models/contentmodels.py index 3265c5c8cc6..6a1a69d4d5d 100644 --- a/cms/models/contentmodels.py +++ b/cms/models/contentmodels.py @@ -226,7 +226,7 @@ def get_placeholder_slots(self): self .get_ancestor_titles() .exclude(template=constants.TEMPLATE_INHERITANCE_MAGIC) - .order_by('-page__node__path') + .order_by('-page__path') .values_list('template', flat=True) ) if templates: diff --git a/cms/page_rendering.py b/cms/page_rendering.py index 0dccef4faf8..34035815ff6 100644 --- a/cms/page_rendering.py +++ b/cms/page_rendering.py @@ -61,17 +61,15 @@ def render_page(request, page, current_language, slug=None): def _handle_no_page(request): try: - # redirect to PageContent's changelist if the root page is detected - resolved_path = resolve(request.path) - if resolved_path.url_name == 'pages-root': - redirect_url = admin_reverse('cms_pagecontent_changelist') - return HttpResponseRedirect(redirect_url) - - # add a $ to the end of the url (does not match on the cms anymore) - return resolve('%s$' % request.path).func(request) + match = resolve(request.path) except Resolver404 as e: - # raise a django http 404 page - raise Http404(dict(path=request.path, tried=e.args[0]['tried'])) + raise Http404(dict(path=request.path, tried=e.args[0]['tried'])) from e + + # redirect to PageContent's changelist if the root page is detected + if match.url_name == 'pages-root': + redirect_url = admin_reverse('cms_pagecontent_changelist') + return HttpResponseRedirect(redirect_url) + raise Http404(dict(path=request.path, tried=match.tried)) def _handle_no_apphook(request): 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/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..38a2a9e2c90 100644 --- a/cms/static/cms/js/widgets/forms.apphookselect.js +++ b/cms/static/cms/js/widgets/forms.apphookselect.js @@ -10,30 +10,42 @@ __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 || {}; + const $ = require('jquery'); + let apphookData = { + apphooks_configuration: {}, + apphooks_configuration_value: undefined, + apphooks_configuration_url: {} + }; // 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] === window.apphooks_configuration_value) { + if (apphooks_configuration[opt.val()][i][0] === apphookData.apphooks_configuration_value) { selectedCfgs = 'selected="selected"'; } appCfgs.append( @@ -42,11 +54,15 @@ 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 (ev) { + ev.preventDefault(); + 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/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)) 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_admin.py b/cms/tests/test_admin.py index 4c724bb1dc8..04eb307f135 100644 --- a/cms/tests/test_admin.py +++ b/cms/tests/test_admin.py @@ -436,6 +436,18 @@ def test_template_inheritance_magic_with_three_levels_of_inheritance_inherits_fr child_page_content = PageContent.objects.get(page=child_page, language="en") self.assertEqual(child_page_content.get_template(), "nav_playground.html") + @override_settings(CMS_TEMPLATES=[]) + def test_placeholder_slot_inheritance_magic_with_two_levels_of_inheritance_without_configured_templates(self): + admin_user = self.get_superuser() + parent_page = create_page("grandparent-page", TEMPLATE_INHERITANCE_MAGIC, "en", created_by=admin_user) + child_page = create_page( + "parent-page", TEMPLATE_INHERITANCE_MAGIC, "en", created_by=admin_user, parent=parent_page + ) + + child_slots = child_page.get_content_obj("en").get_placeholder_slots() + + self.assertEqual(child_slots, ("content",)) + class AdminTests(AdminTestsBase): # TODO: needs tests for actual permissions, not only superuser/normaluser 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""" 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, - '