from collections import deque from django.template import Context from django.test.utils import override_settings from cms.api import add_plugin, create_page from cms.models import CMSPlugin, Placeholder from cms.plugin_rendering import ( ContentRenderer, LegacyRenderer, StructureRenderer, ) from cms.test_utils.testcases import CMSTestCase from cms.test_utils.util.context_managers import override_placeholder_conf from cms.toolbar.toolbar import CMSToolbar from cms.toolbar.utils import get_object_edit_url class TestStructureRenderer(CMSTestCase): renderer_class = StructureRenderer def get_renderer(self, path=None, language='en', page=None): if page and path is None: path = page.get_absolute_url(language) request = self.get_request(path, language, page=page) return self.renderer_class(request) def test_get_plugin_class_cache(self): plugin = CMSPlugin(plugin_type='MultiColumnPlugin') renderer = self.get_renderer() plugin_class = renderer.get_plugin_class(plugin) self.assertIn('MultiColumnPlugin', renderer._cached_plugin_classes) self.assertEqual(plugin_class.__name__, 'MultiColumnPlugin') self.assertEqual(renderer._cached_plugin_classes['MultiColumnPlugin'], plugin_class) def test_get_placeholder_plugin_menu(self): cms_page = create_page("page", 'nav_playground.html', "en") superuser = self.get_superuser() placeholder_1 = cms_page.get_placeholders("en").get(slot='body') with self.login_user_context(superuser): renderer = self.get_renderer() plugin_menu = renderer.get_placeholder_plugin_menu(placeholder_1) expected = '
Multi Columns
' self.assertTrue(expected in plugin_menu) def test_render_placeholder_toolbar_js(self): cms_page = create_page("page", 'nav_playground.html', "en") renderer = self.get_renderer() placeholder = cms_page.get_placeholders("en").get(slot='body') content = renderer.get_placeholder_toolbar_js(placeholder) expected_bits = [ '"MultiColumnPlugin"', '"addPluginHelpTitle": "Add plugin to placeholder \\"Body\\""', '"name": "Body"', f'"placeholder_id": "{placeholder.pk}"', ] for bit in expected_bits: self.assertIn(bit, content) def test_render_placeholder_toolbar_js_escaping(self): cms_page = create_page("page", 'nav_playground.html', "en") renderer = self.get_renderer() placeholder = cms_page.get_placeholders("en").get(slot='body') conf = {placeholder.slot: {'name': 'Content-with-dash'}} with override_placeholder_conf(CMS_PLACEHOLDER_CONF=conf): content = renderer.get_placeholder_toolbar_js(placeholder) expected_bits = [ '"MultiColumnPlugin"', '"addPluginHelpTitle": "Add plugin to placeholder \\"Content-with-dash\\""', '"name": "Content-with-dash"', f'"placeholder_id": "{placeholder.pk}"', ] for bit in expected_bits: self.assertIn(bit, content) class TestContentRenderer(TestStructureRenderer): renderer_class = ContentRenderer @override_settings(CMS_PLACEHOLDER_CACHE=True) def test_placeholder_cache_enabled(self): cms_page = create_page("page", 'nav_playground.html', "en") request_path = cms_page.get_absolute_url('en') renderer = self.get_renderer(request_path, page=cms_page) self.assertTrue(renderer.placeholder_cache_is_enabled()) @override_settings(CMS_PLACEHOLDER_CACHE=True) def test_placeholder_cache_disabled(self): with override_settings(CMS_PLACEHOLDER_CACHE=False): # Placeholder cache has been explicitly disabled renderer = self.get_renderer() self.assertFalse(renderer.placeholder_cache_is_enabled()) with self.login_user_context(self.get_staff_user_with_no_permissions()): # Placeholder cache is disabled for all staff users renderer = self.get_renderer() self.assertFalse(renderer.placeholder_cache_is_enabled()) def test_preload_placeholders_for_page_with_inherit_off(self): cms_page = create_page("page", 'nav_playground.html', "en") placeholder_1 = cms_page.get_placeholders("en").get(slot='body') placeholder_1_plugin_1 = add_plugin( placeholder_1, plugin_type='LinkPlugin', language='en', name='Link #1', external_link='https://www.django-cms.org', ) placeholder_1_plugin_2 = add_plugin( placeholder_1, plugin_type='LinkPlugin', language='en', name='Link #2', external_link='https://www.django-cms.org', ) placeholder_2 = cms_page.get_placeholders("en").get(slot='right-column') placeholder_2_plugin_1 = add_plugin( placeholder_2, plugin_type='LinkPlugin', language='en', name='Link #3', external_link='https://www.django-cms.org', ) renderer = self.get_renderer(page=cms_page) renderer._preload_placeholders_for_page(cms_page) self.assertIn(cms_page.pk, renderer._placeholders_by_page_cache) self.assertIn(placeholder_1.slot, renderer._placeholders_by_page_cache[cms_page.pk]) self.assertIn(placeholder_2.slot, renderer._placeholders_by_page_cache[cms_page.pk]) cache = renderer._placeholders_by_page_cache[cms_page.pk] self.assertEqual(cache[placeholder_1.slot], placeholder_1) self.assertEqual( cache[placeholder_1.slot]._plugins_cache, deque([placeholder_1_plugin_1, placeholder_1_plugin_2]) ) self.assertEqual(cache[placeholder_2.slot], placeholder_2) self.assertEqual(cache[placeholder_2.slot]._plugins_cache, deque([placeholder_2_plugin_1])) class TestExceptionCatchers(CMSTestCase): renderer_class = ContentRenderer def setUp(self): self.user = self.get_superuser() self.cms_page = create_page("page", 'nav_playground.html', "en") self.placeholder_1 = self.cms_page.get_placeholders("en").get(slot='body') add_plugin( self.placeholder_1, plugin_type='LinkPlugin', language='en', name='Link #1', external_link='https://www.django-cms.org', ) add_plugin( self.placeholder_1, plugin_type='BuggyPlugin', language='en', ) self.placeholder_2 = self.cms_page.get_placeholders("en").get(slot='right-column') self.placeholder_2_plugin_1 = add_plugin( self.placeholder_2, plugin_type='LinkPlugin', language='en', name='Link #3', external_link='https://www.django-cms.org', ) self.request = self.get_request( get_object_edit_url(self.cms_page.get_admin_content('en')), 'en', page=self.cms_page, ) self.request.toolbar = CMSToolbar(self.request) self.renderer = self.renderer_class(self.request) def test_non_existent_plugins_creates_error_log_and_does_not_fail(self): self.placeholder_2_plugin_1.plugin_type = 'NonExistingPlugin' self.placeholder_2_plugin_1.save() with self.assertLogs("cms.utils.plugins", level="ERROR") as logs: plugin_context = Context() self.renderer.render_placeholder(self.placeholder_2, plugin_context, "en") self.assertEqual(len(logs.output), 1) self.assertIn("Plugin not installed: NonExistingPlugin", logs.output[0]) self.placeholder_2_plugin_1.plugin_type = 'LinkPlugin' self.placeholder_2_plugin_1.save() def test_exception_in_plugin_render_creates_error_log_and_does_not_fail(self): with self.assertLogs("cms.plugin_rendering", level="ERROR") as logs: plugin_context = Context() self.renderer.render_placeholder(self.placeholder_1, plugin_context, "en") self.assertEqual(len(logs.output), 1) self.assertIn("ZeroDivisionError: division by zero", logs.output[0]) def test_exception_in_template_rendering_creates_error_log_and_does_not_fail(self): from cms.test_utils.project.pluginapp.plugins.link.cms_plugins import ( LinkPlugin, ) link_template = LinkPlugin.render_template LinkPlugin.render_template = "pluginapp/link/bugs.html" with self.assertLogs("cms.plugin_rendering", level="ERROR") as logs: plugin_context = Context() self.renderer.render_placeholder(self.placeholder_1, plugin_context, "en") self.assertEqual(len(logs.output), 1) self.assertIn("TemplateSyntaxError", logs.output[0]) self.assertIn("Invalid block tag on line 1", logs.output[0]) LinkPlugin.render_template = link_template def test_exception_in_plugin_render_shows_error_in_edit_mode(self): plugin_context = Context() with self.assertLogs("cms.plugin_rendering", level="ERROR"): markup = self.renderer.render_placeholder(self.placeholder_1, plugin_context, "en", editable=True) self.assertTrue(self.renderer.toolbar.edit_mode_active) self.assertIn('
', markup) self.assertIn("ZeroDivisionError", markup) self.assertIn("division by zero", markup) def test_exception_in_plugin_render_escapes_user_content_in_edit_mode(self): # The exception heading interpolates user-controlled data such as the # object title (``str(placeholder.source)``). It must be HTML-escaped # before being returned as safe markup, otherwise a stored payload # would execute in a staff user's browser in edit mode. payload = '' page_content = self.cms_page.get_admin_content('en') page_content.title = payload page_content.save() # Re-fetch the placeholder so its (generic) ``source`` is reloaded with # the updated title rather than the value cached during ``setUp``. placeholder = Placeholder.objects.get(pk=self.placeholder_1.pk) plugin_context = Context() with self.assertLogs("cms.plugin_rendering", level="ERROR"): markup = self.renderer.render_placeholder(placeholder, plugin_context, "en", editable=True) self.assertIn('
', markup) self.assertNotIn(payload, markup) self.assertIn('<img src=x onerror=alert(1)>', markup) def test_exception_in_plugin_render_is_silent_in_preview_mode(self): plugin_context = Context() with self.assertLogs("cms.plugin_rendering", level="ERROR"): markup = self.renderer.render_placeholder(self.placeholder_1, plugin_context, "en", editable=False) self.assertEqual('', markup) def test_exception_in_plugin_render_is_silent_in_live_mode(self): plugin_context = Context() self.request = self.get_request( self.cms_page.get_absolute_url('en'), 'en', page=self.cms_page, ) self.request.toolbar = CMSToolbar(self.request) self.renderer = self.renderer_class(self.request) with self.assertLogs("cms.plugin_rendering", level="ERROR"): markup = self.renderer.render_placeholder(self.placeholder_1, plugin_context, "en", editable=False) self.assertFalse(self.renderer.toolbar.edit_mode_active) self.assertEqual('', markup) @override_settings(CMS_CATCH_PLUGIN_500_EXCEPTION=False) def test_exception_in_plugin_render_is_raised__in_live_mode(self): plugin_context = Context() self.request = self.get_request( self.cms_page.get_absolute_url('en'), 'en', page=self.cms_page, ) self.request.toolbar = CMSToolbar(self.request) self.renderer = self.renderer_class(self.request) with self.assertRaises(ZeroDivisionError): with self.assertLogs("cms.plugin_rendering", level="ERROR"): self.renderer.render_placeholder(self.placeholder_1, plugin_context, "en", editable=False) class TestLegacyRenderer(TestContentRenderer): renderer_class = LegacyRenderer class TestLegacyRendererExceptionCatcher(TestExceptionCatchers): renderer_class = LegacyRenderer class TestValidModelsFiltering(CMSTestCase): """Tests that get_placeholder_toolbar_js only lists plugins valid for the placeholder's source model. We create a placeholder on a non-page model (Example1) and register two temporary plugins: - AllowedExamplePlugin: allowed_models explicitly lists the Example1 model. - DisallowedExamplePlugin: allowed_models lists a different model path. The toolbar js should include AllowedExamplePlugin but exclude DisallowedExamplePlugin. """ def setUp(self): from cms.plugin_base import CMSPluginBase from cms.plugin_pool import plugin_pool from cms.test_utils.project.placeholderapp.models import Example1 self.example = Example1.objects.create( char_1="ex1", char_2="ex2", char_3="ex3", char_4="ex4", publish=True ) self.placeholder = self.example.placeholder # dynamic registration of two plugins with differing allowed_models class AllowedExamplePlugin(CMSPluginBase): name = "AllowedExamplePlugin" render_template = "cms/content.html" allowed_models = ["placeholderapp.example1"] class DisallowedExamplePlugin(CMSPluginBase): name = "DisallowedExamplePlugin" render_template = "cms/content.html" allowed_models = ["placeholderapp.someothermodel"] self.AllowedExamplePlugin = AllowedExamplePlugin self.DisallowedExamplePlugin = DisallowedExamplePlugin plugin_pool.register_plugin(AllowedExamplePlugin) plugin_pool.register_plugin(DisallowedExamplePlugin) def tearDown(self): # Unregister temporary plugins to avoid leaking into other tests from cms.plugin_pool import plugin_pool plugin_pool.unregister_plugin(self.AllowedExamplePlugin) plugin_pool.unregister_plugin(self.DisallowedExamplePlugin) def test_get_placeholder_toolbar_js_allowed_models_filtering(self): from cms.plugin_rendering import ContentRenderer request = self.get_request("/", "en") renderer = ContentRenderer(request) js = renderer.get_placeholder_toolbar_js(self.placeholder) # Allowed plugin should appear self.assertIn('"AllowedExamplePlugin"', js) # Disallowed plugin must not appear self.assertNotIn('"DisallowedExamplePlugin"', js) # Sanity check: placeholder id present self.assertIn(f'"placeholder_id": "{self.placeholder.pk}"', js) def test_get_placeholder_toolbar_js_pagecontent_allowed_models(self): from cms.api import create_page from cms.plugin_base import CMSPluginBase from cms.plugin_pool import plugin_pool from cms.plugin_rendering import ContentRenderer # Create a CMS page placeholder as source model cms.pagecontent cms_page = create_page("page", 'nav_playground.html', "en") page_placeholder = cms_page.get_placeholders("en").get(slot='body') class PageOnlyTmpPlugin(CMSPluginBase): name = "PageOnlyTmpPlugin" render_template = "cms/content.html" allowed_models = ["cms.pagecontent"] try: plugin_pool.register_plugin(PageOnlyTmpPlugin) # For a page placeholder, the page-only plugin should be present request = self.get_request(cms_page.get_absolute_url('en'), 'en', page=cms_page) renderer = ContentRenderer(request) js_page = renderer.get_placeholder_toolbar_js(page_placeholder) self.assertIn('"PageOnlyTmpPlugin"', js_page) # For a non-page model placeholder, it must be excluded request2 = self.get_request('/', 'en') renderer2 = ContentRenderer(request2) js_obj = renderer2.get_placeholder_toolbar_js(self.placeholder) self.assertNotIn('"PageOnlyTmpPlugin"', js_obj) finally: plugin_pool.unregister_plugin(PageOnlyTmpPlugin) class TestAllowedPluginsFiltering(CMSTestCase): """Tests for model-level plugin filtering via the allowed_plugins attribute. Tests that get_placeholder_toolbar_js and get_all_plugins_for_model respect the allowed_plugins property on models. This is a model-level restriction that works in combination with the plugin-level allowed_models attribute. Model-level filtering (allowed_plugins on model): - None (default): All plugins are allowed (subject to plugin's allowed_models) - List of plugin names: Only those plugins are allowed - Empty list []: No plugins are allowed We create a placeholder on a model with an allowed_plugins property and verify that only those plugins appear in the toolbar and are returned by get_all_plugins_for_model. """ def setUp(self): from cms.plugin_base import CMSPluginBase from cms.plugin_pool import plugin_pool from cms.test_utils.project.placeholderapp.models import Example1 # Create an Example1 instance self.example = Example1.objects.create( char_1="ex1", char_2="ex2", char_3="ex3", char_4="ex4", publish=True ) self.placeholder = self.example.placeholder # Register temporary test plugins class AllowedPluginA(CMSPluginBase): name = "AllowedPluginA" render_template = "cms/content.html" class AllowedPluginB(CMSPluginBase): name = "AllowedPluginB" render_template = "cms/content.html" class DisallowedPlugin(CMSPluginBase): name = "DisallowedPlugin" render_template = "cms/content.html" self.AllowedPluginA = AllowedPluginA self.AllowedPluginB = AllowedPluginB self.DisallowedPlugin = DisallowedPlugin plugin_pool.register_plugin(AllowedPluginA) plugin_pool.register_plugin(AllowedPluginB) plugin_pool.register_plugin(DisallowedPlugin) def tearDown(self): from cms.plugin_pool import plugin_pool plugin_pool.unregister_plugin(self.AllowedPluginA) plugin_pool.unregister_plugin(self.AllowedPluginB) plugin_pool.unregister_plugin(self.DisallowedPlugin) def test_get_all_plugins_for_model_without_allowed_plugins(self): """Test that models without allowed_plugins (None) return all plugins. When allowed_plugins is None (not defined), no model-level filtering occurs. Only plugin-level filtering (allowed_models) is applied. """ from cms.plugin_pool import plugin_pool from cms.test_utils.project.placeholderapp.models import Example1 all_plugins = plugin_pool.get_all_plugins_for_model(Example1) plugin_names = [p.__name__ for p in all_plugins] # All three test plugins should be included self.assertIn('AllowedPluginA', plugin_names) self.assertIn('AllowedPluginB', plugin_names) self.assertIn('DisallowedPlugin', plugin_names) def test_get_all_plugins_for_model_with_allowed_plugins(self): """Test that allowed_plugins property applies model-level filtering. When allowed_plugins is a list of plugin names, only those plugins (and those passing the allowed_models filter) are returned. """ from cms.plugin_pool import plugin_pool from cms.test_utils.project.placeholderapp.models import Example1 # Temporarily add allowed_plugins to Example1 Example1.allowed_plugins = ['AllowedPluginA', 'AllowedPluginB'] # Clear the cache since get_all_plugins_for_model uses @lru_cache plugin_pool.get_all_plugins_for_model.cache_clear() try: filtered_plugins = plugin_pool.get_all_plugins_for_model(Example1) plugin_names = [p.__name__ for p in filtered_plugins] # Only allowed plugins should be included self.assertIn('AllowedPluginA', plugin_names) self.assertIn('AllowedPluginB', plugin_names) self.assertNotIn('DisallowedPlugin', plugin_names) finally: # Clean up delattr(Example1, 'allowed_plugins') plugin_pool.get_all_plugins_for_model.cache_clear() def test_get_all_plugins_for_model_with_empty_allowed_plugins(self): """Test that empty allowed_plugins list returns no plugins. Semantic difference: - None: No model-level restriction (all plugins allowed subject to allowed_models) - []: Explicit restriction to allow no plugins - ['PluginA']: Explicit restriction to specific plugins """ from cms.plugin_pool import plugin_pool from cms.test_utils.project.placeholderapp.models import Example1 Example1.allowed_plugins = [] plugin_pool.get_all_plugins_for_model.cache_clear() try: filtered_plugins = plugin_pool.get_all_plugins_for_model(Example1) # Empty list should result in no plugins self.assertEqual(len(filtered_plugins), 0) finally: delattr(Example1, 'allowed_plugins') plugin_pool.get_all_plugins_for_model.cache_clear() def test_get_placeholder_toolbar_js_with_allowed_plugins(self): """Test that toolbar JS respects model-level allowed_plugins filtering. The toolbar should only show plugins that pass both the plugin's allowed_models filter and the model's allowed_plugins filter. """ from cms.plugin_rendering import ContentRenderer from cms.test_utils.project.placeholderapp.models import Example1 Example1.allowed_plugins = ['AllowedPluginA', 'AllowedPluginB'] try: request = self.get_request("/", "en") renderer = ContentRenderer(request) js = renderer.get_placeholder_toolbar_js(self.placeholder) # Allowed plugins should appear self.assertIn('"AllowedPluginA"', js) self.assertIn('"AllowedPluginB"', js) # Disallowed plugin must not appear self.assertNotIn('"DisallowedPlugin"', js) finally: delattr(Example1, 'allowed_plugins') from cms.plugin_pool import plugin_pool plugin_pool.get_all_plugins_for_model.cache_clear() def test_allowed_plugins_with_nonexistent_plugin(self): """Test that nonexistent plugins in allowed_plugins are safely ignored. If allowed_plugins contains plugin names that don't exist or aren't registered, they are silently ignored without raising errors. """ from cms.plugin_pool import plugin_pool from cms.test_utils.project.placeholderapp.models import Example1 Example1.allowed_plugins = ['AllowedPluginA', 'NonExistentPlugin'] plugin_pool.get_all_plugins_for_model.cache_clear() try: filtered_plugins = plugin_pool.get_all_plugins_for_model(Example1) plugin_names = [p.__name__ for p in filtered_plugins] # Only the existing allowed plugin should be included self.assertIn('AllowedPluginA', plugin_names) self.assertNotIn('AllowedPluginB', plugin_names) self.assertNotIn('DisallowedPlugin', plugin_names) # No error should be raised for nonexistent plugin finally: delattr(Example1, 'allowed_plugins') plugin_pool.get_all_plugins_for_model.cache_clear() def test_allowed_plugins_combined_with_allowed_models(self): """Test that both allowed_plugins and allowed_models filters work together. For a plugin to be available on a model, it must pass both filters: 1. Plugin-level: If plugin.allowed_models is set, model must be in the list 2. Model-level: If model.allowed_plugins is set, plugin must be in the list If either filter excludes the plugin, it won't be available. """ from cms.plugin_base import CMSPluginBase from cms.plugin_pool import plugin_pool from cms.test_utils.project.placeholderapp.models import Example1 # Register a plugin with allowed_models restriction class RestrictedModelPlugin(CMSPluginBase): name = "RestrictedModelPlugin" render_template = "cms/content.html" allowed_models = ["placeholderapp.example1"] class WrongModelPlugin(CMSPluginBase): name = "WrongModelPlugin" render_template = "cms/content.html" allowed_models = ["placeholderapp.someothermodel"] plugin_pool.register_plugin(RestrictedModelPlugin) plugin_pool.register_plugin(WrongModelPlugin) Example1.allowed_plugins = ['AllowedPluginA', 'RestrictedModelPlugin', 'WrongModelPlugin'] plugin_pool.get_all_plugins_for_model.cache_clear() try: filtered_plugins = plugin_pool.get_all_plugins_for_model(Example1) plugin_names = [p.__name__ for p in filtered_plugins] # AllowedPluginA should be included (in allowed_plugins, no model restriction) self.assertIn('AllowedPluginA', plugin_names) # RestrictedModelPlugin should be included (in allowed_plugins AND correct allowed_models) self.assertIn('RestrictedModelPlugin', plugin_names) # WrongModelPlugin should be excluded (in allowed_plugins but wrong allowed_models) self.assertNotIn('WrongModelPlugin', plugin_names) # AllowedPluginB should be excluded (not in allowed_plugins) self.assertNotIn('AllowedPluginB', plugin_names) finally: delattr(Example1, 'allowed_plugins') plugin_pool.unregister_plugin(RestrictedModelPlugin) plugin_pool.unregister_plugin(WrongModelPlugin) plugin_pool.get_all_plugins_for_model.cache_clear()