import datetime
import pickle
import warnings
from contextlib import contextmanager
from unittest import mock, skipIf
from django import http
from django.conf import settings
from django.contrib import admin
from django.contrib.admin.options import IS_POPUP_VAR
from django.contrib.admin.widgets import FilteredSelectMultiple, RelatedFieldWidgetWrapper
from django.core.exceptions import ImproperlyConfigured
from django.forms.widgets import Media
from django.test.testcases import TestCase
from django.urls import re_path, reverse
from django.utils import timezone
from django.utils.encoding import force_str
from django.utils.translation import override as force_language
from djangocms_text.models import Text
from cms import api
from cms.api import create_page
from cms.exceptions import DontUsePageAttributeWarning, PluginAlreadyRegistered, PluginNotRegistered
from cms.models import Page, Placeholder
from cms.models.pluginmodel import CMSPlugin
from cms.plugin_base import CMSPluginBase
from cms.plugin_pool import plugin_pool
from cms.sitemaps.cms_sitemap import CMSSitemap
from cms.test_utils.project.pluginapp.plugins.manytomany_rel.models import Article, ArticlePluginModel, Section
from cms.test_utils.project.pluginapp.plugins.meta.cms_plugins import (
TestPlugin,
TestPlugin2,
TestPlugin3,
TestPlugin4,
TestPlugin5,
)
from cms.test_utils.project.pluginapp.plugins.validation.cms_plugins import (
DynTemplate,
NonExisitngRenderTemplate,
NoRender,
NoRenderButChildren,
)
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
from cms.utils.plugins import copy_plugins_to_placeholder, get_plugins
from cms.utils.urlutils import admin_reverse
@contextmanager
def register_plugins(*plugins):
for plugin in plugins:
plugin_pool.register_plugin(plugin)
# clear cached properties
plugin_pool._clear_cached()
try:
yield
finally:
for plugin in plugins:
plugin_pool.unregister_plugin(plugin)
class DumbFixturePlugin(CMSPluginBase):
model = CMSPlugin
name = "Dumb Test Plugin. It does nothing."
render_template = ""
admin_preview = False
render_plugin = False
def render(self, context, instance, placeholder):
return context
class DirectProxyCMSPluginModel(CMSPlugin):
class Meta:
proxy = True
app_label = "test_app"
class DirectProxyCMSPlugin(CMSPluginBase):
model = DirectProxyCMSPluginModel
name = "Direct proxy CMSPlugin"
render_plugin = False
class DumbFixturePluginWithUrls(DumbFixturePlugin):
name = DumbFixturePlugin.name + " With custom URLs."
render_plugin = False
def _test_view(self, request):
return http.HttpResponse("It works")
def get_plugin_urls(self):
return [
re_path(r"^testview/$", admin.site.admin_view(self._test_view), name="dumbfixtureplugin"),
]
plugin_pool.register_plugin(DumbFixturePluginWithUrls)
class PluginsTestBaseCase(CMSTestCase):
def setUp(self):
plugin_pool._clear_cached()
self.super_user = self._create_user("test", True, True)
self.slave = self._create_user("slave", True)
self.FIRST_LANG = settings.LANGUAGES[0][0]
self.SECOND_LANG = settings.LANGUAGES[1][0]
self._login_context = self.login_user_context(self.super_user)
self._login_context.__enter__()
def tearDown(self):
self._login_context.__exit__(None, None, None)
def get_request(self, *args, **kwargs):
request = super().get_request(*args, **kwargs)
request.placeholder_media = Media()
request.toolbar = CMSToolbar(request)
return request
def get_response_pk(self, response):
return int(response.content.decode("utf8").split("/edit-plugin/")[1].split("/")[0])
def get_placeholder(self):
return Placeholder.objects.create(slot="test")
class PluginsTestCase(PluginsTestBaseCase):
def _create_link_plugin_on_page(self, page, slot="col_left"):
add_url = self.get_add_plugin_uri(
placeholder=page.get_placeholders("en").get(slot=slot),
plugin_type="LinkPlugin",
language=settings.LANGUAGES[0][0],
)
data = {"name": "A Link", "external_link": "https://www.django-cms.org"}
response = self.client.post(add_url, data)
self.assertEqual(response.status_code, 200)
return CMSPlugin.objects.latest("pk")
def __edit_link_plugin(self, plugin, text):
endpoint = self.get_change_plugin_uri(plugin)
response = self.client.get(endpoint)
self.assertEqual(response.status_code, 200)
data = {"name": text, "external_link": "https://www.django-cms.org"}
response = self.client.post(endpoint, data)
self.assertEqual(response.status_code, 200)
return CMSPlugin.objects.get(pk=plugin.pk).get_bound_plugin()
def test_add_edit_plugin(self):
"""
Test that you can add a text plugin
"""
# add a new text plugin
page_data = self.get_new_page_data()
self.client.post(self.get_page_add_uri("en"), page_data)
page = Page.objects.first()
created_plugin = self._create_link_plugin_on_page(page)
# now edit the plugin
plugin = self.__edit_link_plugin(created_plugin, "Hello World")
self.assertEqual("Hello World", plugin.name)
def test_plugin_add_form_integrity(self):
admin.autodiscover()
admin_instance = admin.site._registry[ArticlePluginModel]
placeholder = self.get_placeholder()
add_url = self.get_add_plugin_uri(placeholder, plugin_type="ArticlePlugin", language=settings.LANGUAGES[0][0])
superuser = self.get_superuser()
plugin = plugin_pool.get_plugin("ArticlePlugin")
with self.login_user_context(superuser):
request = self.get_request(add_url)
PluginFormClass = plugin(
model=plugin.model,
admin_site=admin.site,
).get_form(request)
plugin_fields = list(PluginFormClass.base_fields.keys())
OriginalFormClass = admin_instance.get_form(request)
original_fields = list(OriginalFormClass.base_fields.keys())
# Assert both forms have the same fields
self.assertEqual(plugin_fields, original_fields)
# Now assert the plugin form has the related field wrapper
# widget on the sections field.
self.assertIsInstance(
PluginFormClass.base_fields["sections"].widget,
RelatedFieldWidgetWrapper,
)
# Now assert the admin form has the related field wrapper
# widget on the sections field.
self.assertIsInstance(
OriginalFormClass.base_fields["sections"].widget,
RelatedFieldWidgetWrapper,
)
# Now assert the plugin form has the filtered select multiple
# widget wrapped by the related field wrapper
self.assertIsInstance(
PluginFormClass.base_fields["sections"].widget.widget,
FilteredSelectMultiple,
)
# Now assert the admin form has the filtered select multiple
# widget wrapped by the related field wrapper
self.assertIsInstance(
OriginalFormClass.base_fields["sections"].widget.widget,
FilteredSelectMultiple,
)
def test_restricted_plugin(self):
CMS_PLACEHOLDER_CONF = {"body": {"plugins": ["TextPlugin"]}}
add_page_endpoint = self.get_page_add_uri("en")
# try to add a new plugin
with override_placeholder_conf(CMS_PLACEHOLDER_CONF=CMS_PLACEHOLDER_CONF):
page_data = self.get_new_page_data()
self.client.post(add_page_endpoint, page_data)
page_content = Page.objects.first().get_admin_content("en")
installed_plugins = plugin_pool.get_all_plugins("body", page_content)
installed_plugins = [cls.__name__ for cls in installed_plugins]
self.assertEqual(["TextPlugin"], installed_plugins)
# try to add a new plugin to a column plugin - still only text
with override_placeholder_conf(CMS_PLACEHOLDER_CONF=CMS_PLACEHOLDER_CONF):
from cms.test_utils.project.pluginapp.plugins.multicolumn.cms_plugins import ColumnPlugin
child_plugins = ColumnPlugin.get_child_classes("body", page_content)
self.assertEqual(["TextPlugin"], child_plugins)
def test_restricted_placeholder_allows_explicit_child_classes(self):
"""Children named explicitly in child_classes do not need to be listed in the
placeholder's plugins -- and do not become available at its root (#8432)"""
from cms.test_utils.project.pluginapp.plugins.multicolumn.cms_plugins import ColumnPlugin, MultiColumnPlugin
page_content = api.create_page("page", "nav_playground.html", "en").get_admin_content("en")
placeholder = page_content.get_placeholders().get(slot="body")
CMS_PLACEHOLDER_CONF = {
"body": {
"plugins": ["MultiColumnPlugin"],
"child_classes": {"ColumnPlugin": ["TextPlugin", "LinkPlugin"]},
}
}
with override_placeholder_conf(CMS_PLACEHOLDER_CONF=CMS_PLACEHOLDER_CONF):
plugin_pool._clear_cached()
# Class-level child_classes
self.assertEqual(["ColumnPlugin"], MultiColumnPlugin.get_child_classes("body", page_content))
# Placeholder-level child_classes (transitively below MultiColumnPlugin)
self.assertEqual(
["LinkPlugin", "TextPlugin"], sorted(ColumnPlugin.get_child_classes("body", page_content))
)
# Only the placeholder's plugins are available at its root
self.assertEqual(
["MultiColumnPlugin"], [plugin.__name__ for plugin in plugin_pool.get_root_plugins(placeholder)]
)
def test_restricted_placeholder_allows_explicit_parent_classes(self):
"""Children naming a parent explicitly in parent_classes do not need to be listed in the
placeholder's plugins, but unrestricted parents still only get the placeholder's plugins"""
ParentPlugin = type(
"ParentPlugin", (CMSPluginBase,), dict(render_plugin=False, allow_children=True)
)
ChildPlugin = type(
"ChildPlugin", (CMSPluginBase,), dict(render_plugin=False, parent_classes=["ParentPlugin"])
)
page_content = api.create_page("page", "nav_playground.html", "en").get_admin_content("en")
CMS_PLACEHOLDER_CONF = {"body": {"plugins": ["ParentPlugin", "TextPlugin"]}}
with register_plugins(ParentPlugin, ChildPlugin):
with override_placeholder_conf(CMS_PLACEHOLDER_CONF=CMS_PLACEHOLDER_CONF):
self.assertEqual(
["ChildPlugin", "ParentPlugin", "TextPlugin"],
sorted(ParentPlugin.get_child_classes("body", page_content)),
)
def test_auto_child_classes_honour_configured_parent_classes(self):
"""child_classes = "auto" also picks up children declaring the parent in CMS_PLACEHOLDER_CONF"""
ParentPlugin = type(
"ParentPlugin", (CMSPluginBase,), dict(render_plugin=False, allow_children=True, child_classes="auto")
)
ChildPlugin = type("ChildPlugin", (CMSPluginBase,), dict(render_plugin=False))
page_content = api.create_page("page", "nav_playground.html", "en").get_admin_content("en")
CMS_PLACEHOLDER_CONF = {
"body": {
"plugins": ["ParentPlugin"],
"parent_classes": {"ChildPlugin": ["ParentPlugin"]},
}
}
with register_plugins(ParentPlugin, ChildPlugin):
with override_placeholder_conf(CMS_PLACEHOLDER_CONF=CMS_PLACEHOLDER_CONF):
self.assertEqual(["ChildPlugin"], ParentPlugin.get_child_classes("body", page_content))
def test_restricted_placeholder_child_classes_from_instance_template(self):
"""Without a page, candidates are resolved for the template of the instance's placeholder source"""
# The template lookup is cached per (pk-equal) instance: Clear entries left by other tests
CMSPluginBase._get_template_for_conf.__func__.cache_clear()
page_content = api.create_page("page", "nav_playground.html", "en").get_admin_content("en")
placeholder = page_content.get_placeholders().get(slot="body")
multi_column = api.add_plugin(placeholder, "MultiColumnPlugin", "en")
column = api.add_plugin(placeholder, "ColumnPlugin", "en", target=multi_column)
CMS_PLACEHOLDER_CONF = {
"body": {"plugins": ["TextPlugin"]},
"nav_playground.html body": {
"plugins": ["MultiColumnPlugin"],
"child_classes": {"ColumnPlugin": ["LinkPlugin"]},
},
}
with override_placeholder_conf(CMS_PLACEHOLDER_CONF=CMS_PLACEHOLDER_CONF):
child_plugins = column.get_plugin_class().get_child_classes("body", None, instance=column)
self.assertEqual(["LinkPlugin"], child_plugins)
def test_restricted_placeholder_explicit_children_respect_exclusions_and_globs(self):
"""excluded_plugins always wins, and glob patterns do not bypass the placeholder's plugins"""
from cms.test_utils.project.pluginapp.plugins.multicolumn.cms_plugins import ColumnPlugin
page_content = api.create_page("page", "nav_playground.html", "en").get_admin_content("en")
CMS_PLACEHOLDER_CONF = {
"body": {
"plugins": ["MultiColumnPlugin"],
"excluded_plugins": ["LinkPlugin"],
"child_classes": {"ColumnPlugin": ["TextPlugin", "LinkPlugin", "Style*"]},
}
}
with override_placeholder_conf(CMS_PLACEHOLDER_CONF=CMS_PLACEHOLDER_CONF):
self.assertEqual(["TextPlugin"], ColumnPlugin.get_child_classes("body", page_content))
def test_excluded_plugin(self):
"""
Test that you can't add a text plugin
"""
CMS_PLACEHOLDER_CONF = {"body": {"excluded_plugins": ["TextPlugin"]}}
add_page_endpoint = self.get_page_add_uri("en")
# try to add a new text plugin
with override_placeholder_conf(CMS_PLACEHOLDER_CONF=CMS_PLACEHOLDER_CONF):
page_data = self.get_new_page_data()
self.client.post(add_page_endpoint, page_data)
page_content = Page.objects.first().get_admin_content("en")
installed_plugins = plugin_pool.get_all_plugins("body", page_content)
installed_plugins = [cls.__name__ for cls in installed_plugins]
self.assertNotIn("TextPlugin", installed_plugins)
CMS_PLACEHOLDER_CONF = {"body": {"plugins": ["TextPlugin"], "excluded_plugins": ["TextPlugin"]}}
# try to add a new text plugin
with override_placeholder_conf(CMS_PLACEHOLDER_CONF=CMS_PLACEHOLDER_CONF):
page_data = self.get_new_page_data()
self.client.post(add_page_endpoint, page_data)
page_content = Page.objects.first().get_admin_content("en")
installed_plugins = plugin_pool.get_all_plugins("body", page_content)
installed_plugins = [cls.__name__ for cls in installed_plugins]
self.assertNotIn("TextPlugin", installed_plugins)
def test_plugin_order(self):
"""
Test that plugin position is saved after creation
"""
page_en = api.create_page("PluginOrderPage", "col_two.html", "en", slug="page1", in_navigation=True)
ph_en = page_en.get_placeholders("en").get(slot="col_left")
# We check created objects and objects from the DB to be sure the position value
# has been saved correctly
text_plugin_1 = api.add_plugin(ph_en, "TextPlugin", "en", body="I'm the first")
text_plugin_2 = api.add_plugin(ph_en, "TextPlugin", "en", body="I'm the second")
db_plugin_1 = CMSPlugin.objects.get(pk=text_plugin_1.pk)
db_plugin_2 = CMSPlugin.objects.get(pk=text_plugin_2.pk)
with self.settings(CMS_PERMISSION=False):
self.assertEqual(text_plugin_1.position, 1)
self.assertEqual(db_plugin_1.position, 1)
self.assertEqual(text_plugin_2.position, 2)
self.assertEqual(db_plugin_2.position, 2)
# Finally we render the placeholder to test the actual content
context = self.get_context(page_en.get_absolute_url(), page=page_en)
rendered_placeholder = self._render_placeholder(ph_en, context)
self.assertEqual(rendered_placeholder, "I'm the firstI'm the second")
def test_plugin_order_alt(self):
"""
Test that plugin position is saved after creation
"""
cms_page = api.create_page("PluginOrderPage", "col_two.html", "en", slug="page1", in_navigation=True)
placeholder = cms_page.get_placeholders("en").get(slot="col_left")
# We check created objects and objects from the DB to be sure the position value
# has been saved correctly
text_plugin_2 = api.add_plugin(placeholder, "TextPlugin", "en", body="I'm the second")
text_plugin_3 = api.add_plugin(placeholder, "TextPlugin", "en", body="I'm the third")
placeholder = cms_page.get_placeholders("en").get(slot="col_left")
# Add a plugin and move it to the first position
text_plugin_1 = api.add_plugin(placeholder, "TextPlugin", "en", body="I'm the first")
data = {
"plugin_id": text_plugin_1.id,
"plugin_parent": "",
"target_language": "en",
"target_position": 1,
}
endpoint = self.get_move_plugin_uri(text_plugin_1)
self.client.post(endpoint, data)
placeholder = cms_page.get_placeholders("en").get(slot="col_left")
with self.settings(CMS_PERMISSION=False):
self.assertEqual(CMSPlugin.objects.get(pk=text_plugin_1.pk).position, 1)
self.assertEqual(CMSPlugin.objects.get(pk=text_plugin_2.pk).position, 2)
self.assertEqual(CMSPlugin.objects.get(pk=text_plugin_3.pk).position, 3)
# Finally we render the placeholder to test the actual content
draft_page_context = self.get_context(cms_page.get_absolute_url(), page=cms_page)
rendered_placeholder = self._render_placeholder(placeholder, draft_page_context)
self.assertEqual(rendered_placeholder, "I'm the firstI'm the secondI'm the third")
def test_plugin_position(self):
page_en = api.create_page("CopyPluginTestPage (EN)", "nav_playground.html", "en")
placeholder = page_en.get_placeholders("en").get(slot="body") # ID 2
placeholder_right = page_en.get_placeholders("en").get(slot="right-column")
columns = api.add_plugin(placeholder, "MultiColumnPlugin", "en") # ID 1
column_1 = api.add_plugin(placeholder, "ColumnPlugin", "en", target=columns) # ID 2
column_2 = api.add_plugin(placeholder, "ColumnPlugin", "en", target=columns) # ID 3
text_1 = api.add_plugin(placeholder, "TextPlugin", "en", target=column_1, body="I'm the first") # ID 4
text_2 = api.add_plugin(placeholder, "TextPlugin", "en", target=column_1, body="I'm the second") # ID 5
returned_1 = copy_plugins_to_placeholder([text_2], placeholder, "en", root_plugin=column_1) # ID 6
returned_2 = copy_plugins_to_placeholder([text_2], placeholder_right, "en") # ID 7
# Column #2 position has changed because of the text plugins above
column_2.refresh_from_db(fields=["position"])
self.assertEqual(column_2.position, 6)
returned_3 = copy_plugins_to_placeholder([text_2], placeholder, "en", root_plugin=column_2) # ID 8
# STATE AT THIS POINT:
# placeholder
# - columns
# - column_1
# - text_plugin "I'm the first" created here
# - text_plugin "I'm the second" created here
# - text_plugin "I'm the second" (returned_1) copied here
# - column_2
# - text_plugin "I'm the second" (returned_3) copied here
# placeholder_right
# - text_plugin "I'm the second" (returned_2) copied here
# First plugin in the plugin branch
self.assertEqual(text_1.position, 3)
# Second plugin in the plugin branch
self.assertEqual(text_2.position, 4)
# Added as third plugin in the same branch as the above
self.assertEqual(returned_1[0].position, 5)
# First plugin in a placeholder
self.assertEqual(returned_2[0].position, 1)
# First plugin nested in a plugin
self.assertEqual(returned_3[0].position, 7)
def test_copy_plugins(self):
"""
Test that copying plugins works as expected.
"""
# create some objects
page_en = api.create_page("CopyPluginTestPage (EN)", "nav_playground.html", "en")
page_de = api.create_page("CopyPluginTestPage (DE)", "nav_playground.html", "de")
ph_en = page_en.get_placeholders("en").get(slot="body")
ph_de = page_de.get_placeholders("de").get(slot="body")
# add the text plugin
text_plugin_en = api.add_plugin(ph_en, "TextPlugin", "en", body="Hello World")
self.assertEqual(text_plugin_en.pk, CMSPlugin.objects.all()[0].pk)
# add a *nested* link plugin
link_plugin_en = api.add_plugin(
ph_en, "LinkPlugin", "en", target=text_plugin_en, name="A Link", external_link="https://www.django-cms.org"
)
# the call above to add a child makes a plugin reload required here.
text_plugin_en = self.reload(text_plugin_en)
# check the relations
self.assertEqual(text_plugin_en.get_children().count(), 1)
self.assertEqual(link_plugin_en.parent.pk, text_plugin_en.pk)
# just sanity check that so far everything went well
self.assertEqual(CMSPlugin.objects.count(), 2)
# copy the plugins to the german placeholder
copy_plugins_to_placeholder(ph_en.get_plugins(), ph_de, language="de")
self.assertEqual(ph_de.cmsplugin_set.filter(parent=None).count(), 1)
text_plugin_de = ph_de.cmsplugin_set.get(parent=None).get_plugin_instance()[0]
self.assertEqual(text_plugin_de.get_children().count(), 1)
link_plugin_de = text_plugin_de.get_children().get().get_plugin_instance()[0]
# check we have twice as many plugins as before
self.assertEqual(CMSPlugin.objects.count(), 4)
# check language plugins
self.assertEqual(CMSPlugin.objects.filter(language="de").count(), 2)
self.assertEqual(CMSPlugin.objects.filter(language="en").count(), 2)
text_plugin_en = self.reload(text_plugin_en)
link_plugin_en = self.reload(link_plugin_en)
# check the relations in english didn't change
self.assertEqual(text_plugin_en.get_children().count(), 1)
self.assertEqual(link_plugin_en.parent.pk, text_plugin_en.pk)
self.assertEqual(link_plugin_de.name, link_plugin_en.name)
self.assertEqual(link_plugin_de.external_link, link_plugin_en.external_link)
self.assertEqual(text_plugin_de.body, text_plugin_en.body)
# test subplugin copy
copy_plugins_to_placeholder([link_plugin_en], ph_de, language="de")
self.assertEqual(ph_de.cmsplugin_set.filter(parent=None).count(), 2)
# Assert that copied plugins have distinct, sequential positions
plugins = list(ph_de.cmsplugin_set.filter(language="de").order_by('position'))
positions = [plugin.position for plugin in plugins]
self.assertEqual(len(positions), len(set(positions)), "Plugin positions should be unique")
self.assertEqual(positions, list(range(positions[0], positions[0] + len(positions))), "Plugin positions should be sequential")
# Assert that no integrity errors are raised (uniqueness constraints maintained)
from django.db import IntegrityError
try:
for plugin in plugins:
# Try to save plugin again to check for uniqueness constraint
plugin.save()
except IntegrityError:
self.fail("IntegrityError raised: uniqueness constraint violated when saving copied plugins")
def test_deep_copy_plugins(self):
page_en = api.create_page("CopyPluginTestPage (EN)", "nav_playground.html", "en")
ph_en = page_en.get_placeholders("en").get(slot="body")
# Grid wrapper 1
mcol1_en = api.add_plugin(ph_en, "MultiColumnPlugin", "en", position="first-child")
# Grid column 1.1
col1_en = api.add_plugin(ph_en, "ColumnPlugin", "en", position="first-child", target=mcol1_en)
# Grid column 1.2
col2_en = api.add_plugin(ph_en, "ColumnPlugin", "en", position="first-child", target=mcol1_en)
# add a *nested* link plugin
link_plugin_en = api.add_plugin(
ph_en, "LinkPlugin", "en", target=col2_en, name="A Link", external_link="https://www.django-cms.org"
)
old_plugins = [mcol1_en, col1_en, col2_en, link_plugin_en]
page_de = api.create_page("CopyPluginTestPage (DE)", "nav_playground.html", "de")
ph_de = page_de.get_placeholders("de").get(slot="body")
# Grid wrapper 1
mcol1_de = api.add_plugin(ph_de, "MultiColumnPlugin", "de", position="first-child")
# Grid column 1.1
col1_de = api.add_plugin(ph_de, "ColumnPlugin", "de", position="first-child", target=mcol1_de)
copy_plugins_to_placeholder(
plugins=[mcol1_en, col1_en, col2_en, link_plugin_en],
placeholder=ph_de,
language="de",
root_plugin=col1_de,
)
col1_de = self.reload(col1_de)
new_plugins = col1_de.get_descendants()
self.assertEqual(new_plugins.count(), len(old_plugins))
for old_plugin, new_plugin in zip(old_plugins, new_plugins, strict=False):
self.assertEqual(old_plugin.get_children().count(), new_plugin.get_children().count())
def test_copy_plugin_without_custom_model(self):
page_en = api.create_page("CopyPluginTestPage (EN)", "nav_playground.html", "en")
page_de = api.create_page("CopyPluginTestPage (DE)", "nav_playground.html", "de")
ph_en = page_en.get_placeholders("en").get(slot="body")
ph_de = page_de.get_placeholders("de").get(slot="body")
api.add_plugin(ph_en, "NoCustomModel", "en")
# sanity check that so far the data matches expectations
self.assertEqual(ph_en.get_plugins("en").count(), 1)
self.assertEqual(ph_en.get_plugins("de").count(), 0)
# copy the plugins to the german placeholder
new_plugins = copy_plugins_to_placeholder(ph_en.get_plugins_list("en"), ph_de, language="de")
new_plugins_qs = [plugin.get_bound_plugin() for plugin in ph_de.get_plugins("de")]
self.assertEqual(len(new_plugins), 1)
self.assertEqual(ph_en.get_plugins("en").count(), 1)
self.assertSequenceEqual(new_plugins_qs, new_plugins)
def test_copy_nested_plugins_without_custom_model(self):
page_en = api.create_page("CopyPluginTestPage (EN)", "nav_playground.html", "en")
page_de = api.create_page("CopyPluginTestPage (DE)", "nav_playground.html", "de")
ph_en = page_en.get_placeholders("en").get(slot="body")
ph_de = page_de.get_placeholders("de").get(slot="body")
grid_plugin = api.add_plugin(ph_en, "MultiColumnPlugin", "en")
column_plugin = api.add_plugin(ph_en, "ColumnPlugin", "en", target=grid_plugin)
api.add_plugin(ph_en, "NoCustomModel", "en", target=column_plugin)
# sanity check that so far the data matches expectations
self.assertEqual(ph_en.get_plugins("en").count(), 3)
self.assertEqual(ph_en.get_plugins("de").count(), 0)
# copy the plugins to the german placeholder
new_plugins = copy_plugins_to_placeholder(ph_en.get_plugins_list("en"), ph_de, language="de")
new_plugins_qs = [plugin.get_bound_plugin() for plugin in ph_de.get_plugins("de")]
self.assertEqual(len(new_plugins), 3)
self.assertEqual(ph_en.get_plugins("en").count(), 3)
self.assertSequenceEqual(new_plugins_qs, new_plugins)
def test_plugin_validation(self):
self.assertRaises(ImproperlyConfigured, plugin_pool.validate_templates, NonExisitngRenderTemplate)
self.assertRaises(ImproperlyConfigured, plugin_pool.validate_templates, NoRender)
self.assertRaises(ImproperlyConfigured, plugin_pool.validate_templates, NoRenderButChildren)
plugin_pool.validate_templates(DynTemplate)
def test_remove_plugin_before_published(self):
"""
When removing a draft plugin we would expect the public copy of the plugin to also be removed
"""
# add a page
page = api.create_page(title="test page", language=settings.LANGUAGES[0][0], template="nav_playground.html")
plugin = api.add_plugin(
placeholder=page.get_placeholders(settings.LANGUAGES[0][0]).get(slot="body"),
language="en",
plugin_type="TextPlugin",
body="",
)
# there should be only 1 plugin
self.assertEqual(CMSPlugin.objects.all().count(), 1)
# delete the plugin
plugin_data = {"plugin_id": plugin.pk}
endpoint = self.get_delete_plugin_uri(plugin)
response = self.client.post(endpoint, plugin_data)
self.assertContains(response, '
')
# there should be no plugins
self.assertEqual(0, CMSPlugin.objects.all().count())
def test_delete_confirmation_views_render_as_popup(self):
"""Both confirmation pages are only ever shown inside the CMS modal, so they
have to render without the admin chrome - like the other frontend editing
views do."""
page = api.create_page(title="test page", language="en", template="nav_playground.html")
placeholder = page.get_placeholders("en").get(slot="body")
plugin = api.add_plugin(placeholder=placeholder, language="en", plugin_type="TextPlugin", body="")
endpoints = (
self.get_delete_plugin_uri(plugin),
admin_reverse("cms_placeholder_clear_placeholder", args=(placeholder.pk,)) + "?language=en",
)
for endpoint in endpoints:
with self.subTest(endpoint=endpoint):
response = self.client.get(endpoint)
self.assertTrue(response.context["is_popup"])
# The admin chrome django renders for a non-popup page
self.assertNotContains(response, 'id="site-name"')
self.assertNotContains(response, 'id="nav-sidebar"')
# ... and the flag has to be available to the confirmation form, so that
# django's stock template can carry it through the POST
self.assertEqual(response.context["is_popup_var"], IS_POPUP_VAR)
def test_remove_plugin_not_associated_to_page(self):
"""
Test case for PlaceholderField
"""
page = api.create_page(title="test page", template="nav_playground.html", language="en")
# add a plugin
plugin = api.add_plugin(
placeholder=page.get_placeholders("en").get(slot="body"),
plugin_type="TextPlugin",
language=settings.LANGUAGES[0][0],
body="",
)
# there should be only 1 plugin
self.assertEqual(CMSPlugin.objects.all().count(), 1)
ph = Placeholder(slot="subplugin")
ph.save()
add_url = self.get_add_plugin_uri(
placeholder=ph, plugin_type="ArticlePlugin", language=settings.LANGUAGES[0][0], parent=plugin
)
response = self.client.post(add_url, {"body": ""})
# no longer allowed for security reasons
self.assertEqual(response.status_code, 400)
def test_register_plugin_twice_should_raise(self):
number_of_plugins_before = len(plugin_pool.registered_plugins)
# The first time we register the plugin is should work
with register_plugins(DumbFixturePlugin):
# Let's add it a second time. We should catch and exception
raised = False
try:
plugin_pool.register_plugin(DumbFixturePlugin)
except PluginAlreadyRegistered:
raised = True
self.assertTrue(raised)
# Let's make sure we have the same number of plugins as before:
number_of_plugins_after = len(plugin_pool.registered_plugins)
self.assertEqual(number_of_plugins_before, number_of_plugins_after)
def test_unregister_non_existing_plugin_should_raise(self):
number_of_plugins_before = len(plugin_pool.registered_plugins)
raised = False
try:
# There should not be such a plugin registered if the others tests
# don't leak plugins
plugin_pool.unregister_plugin(DumbFixturePlugin)
except PluginNotRegistered:
raised = True
self.assertTrue(raised)
# Let's count, to make sure we didn't remove a plugin accidentally.
number_of_plugins_after = len(plugin_pool.registered_plugins)
self.assertEqual(number_of_plugins_before, number_of_plugins_after)
def test_search_pages_for_plugin_content(self):
"""
Test search for pages
To be fully useful, this testcase needs to have the following different
Plugin configurations within the project:
* unaltered cmsplugin_ptr
* cmsplugin_ptr with related_name='+'
* cmsplugin_ptr with related_query_name='+'
* cmsplugin_ptr with related_query_name='whatever_foo'
* cmsplugin_ptr with related_name='whatever_bar'
* cmsplugin_ptr with related_query_name='whatever_foo' and related_name='whatever_bar'
Those plugins are in cms/test_utils/project/pluginapp/revdesc/models.py
"""
page = api.create_page("page", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").get(slot="body")
text = Text(body="hello", language="en", placeholder=placeholder, plugin_type="TextPlugin", position=1)
text.save()
self.assertEqual(Page.objects.search("hi").count(), 0)
self.assertEqual(Page.objects.search("hello").count(), 1)
self.assertEqual(Page.objects.search("hi", language="en").count(), 0)
self.assertEqual(Page.objects.search("hello", language="fr").count(), 0)
def test_empty_plugin_is_ignored(self):
page = api.create_page("page", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").get(slot="body")
CMSPlugin.objects.create(
plugin_type="TextPlugin",
placeholder=placeholder,
position=1,
language=self.FIRST_LANG,
)
# this should not raise any errors, but just ignore the empty plugin
out = self._render_placeholder(placeholder, self.get_context(), width=300)
self.assertFalse(len(out))
self.assertFalse(len(placeholder._plugins_cache))
def test_repr(self):
non_saved_plugin = CMSPlugin()
self.assertIsNone(non_saved_plugin.pk)
self.assertIn("id=None", repr(non_saved_plugin))
self.assertIn("plugin_type=''", repr(non_saved_plugin))
saved_plugin = CMSPlugin.objects.create(plugin_type="TextPlugin")
self.assertIn(f"id={saved_plugin.pk}", repr(saved_plugin))
self.assertIn(f"plugin_type='{saved_plugin.plugin_type}'", repr(saved_plugin))
def test_pickle(self):
page = api.create_page("page", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").get(slot="body")
text_plugin = api.add_plugin(
placeholder,
"TextPlugin",
"en",
body="Hello World",
)
cms_plugin = text_plugin.cmsplugin_ptr
# assert we can pickle and unpickle a solid plugin (subclass)
self.assertEqual(text_plugin, pickle.loads(pickle.dumps(text_plugin)))
# assert we can pickle and unpickle a cms plugin (parent)
self.assertEqual(cms_plugin, pickle.loads(pickle.dumps(cms_plugin)))
def test_defer_pickle(self):
page = api.create_page("page", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").get(slot="body")
api.add_plugin(placeholder, "TextPlugin", "en", body="Hello World")
plugins = Text.objects.all().defer("position")
import io
a = io.BytesIO()
pickle.dump(plugins[0], a)
def test_empty_plugin_description(self):
page = api.create_page("page", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").get(slot="body")
a = CMSPlugin(
plugin_type="TextPlugin",
placeholder=placeholder,
position=1,
language=self.FIRST_LANG,
)
a.save()
self.assertEqual(a.get_short_description(), "")
def test_page_attribute_warns(self):
page = api.create_page("page", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").get(slot="body")
a = CMSPlugin(plugin_type="TextPlugin", placeholder=placeholder, position=1, language=self.FIRST_LANG)
a.save()
def get_page(plugin):
return plugin.page
self.failUnlessWarns(
DontUsePageAttributeWarning,
"Don't use the page attribute on CMSPlugins! "
"CMSPlugins are not guaranteed to have a page associated with them!",
get_page,
a,
)
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
a.page
self.assertEqual(1, len(w))
self.assertIn("test_plugins.py", w[0].filename)
def test_editing_plugin_changes_page_modification_time_in_sitemap(self):
now = timezone.now()
one_day_ago = now - datetime.timedelta(days=1)
page = api.create_page("page", "nav_playground.html", "en")
page.creation_date = one_day_ago
page.changed_date = one_day_ago
page.save()
plugin = self._create_link_plugin_on_page(page, slot="body")
plugin = self.__edit_link_plugin(plugin, "fnord")
sitemap = CMSSitemap()
actual_last_modification_time = sitemap.lastmod(sitemap.items().first())
actual_last_modification_time -= datetime.timedelta(microseconds=actual_last_modification_time.microsecond)
self.assertEqual(plugin.changed_date.date(), actual_last_modification_time.date())
self.assertEqual(page.changed_date.date(), one_day_ago.date() + datetime.timedelta(days=1))
def test_moving_plugin_to_different_placeholder(self):
with register_plugins(DumbFixturePlugin):
page = api.create_page("page", "nav_playground.html", "en")
plugin = api.add_plugin(
placeholder=page.get_placeholders("en").get(slot="body"),
plugin_type="DumbFixturePlugin",
language=settings.LANGUAGES[0][0],
)
child_plugin = api.add_plugin(
placeholder=page.get_placeholders("en").get(slot="body"),
plugin_type="DumbFixturePlugin",
language=settings.LANGUAGES[0][0],
parent=plugin,
)
post = {
"plugin_id": child_plugin.pk,
"placeholder_id": page.get_placeholders("en").get(slot="right-column").pk,
"target_language": "en",
"plugin_parent": "",
"target_position": 1,
}
endpoint = self.get_move_plugin_uri(child_plugin)
response = self.client.post(endpoint, post)
self.assertEqual(response.status_code, 200)
def test_custom_plugin_urls(self):
plugin_url = reverse("admin:dumbfixtureplugin")
response = self.client.get(plugin_url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b"It works")
def test_plugin_require_parent(self):
"""
Assert that a plugin marked as 'require_parent' is not listed
in the plugin pool when a placeholder is specified
"""
ParentRequiredPlugin = type(
"ParentRequiredPlugin", (CMSPluginBase,), dict(require_parent=True, render_plugin=False)
)
with register_plugins(ParentRequiredPlugin):
page = api.create_page("page", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").get(slot="body")
plugin_list = plugin_pool.get_root_plugins(placeholder)
self.assertFalse(ParentRequiredPlugin in plugin_list)
def test_plugin_toolbar_struct(self):
# Tests that the output of the plugin toolbar structure.
page = api.create_page("page", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").get(slot="body")
from cms.utils.placeholder import get_toolbar_plugin_struct
expected_struct_en = {
"module": "Generic",
"name": "Style",
"value": "StylePlugin",
}
expected_struct_de = {
"module": "Generisch",
"name": "Style",
"value": "StylePlugin",
}
toolbar_struct = get_toolbar_plugin_struct(
plugins=plugin_pool.registered_plugins,
slot=placeholder.slot,
page=page,
)
style_config = [config for config in toolbar_struct if config["value"] == "StylePlugin"]
self.assertEqual(len(style_config), 1)
style_config = style_config[0]
with force_language("en"):
self.assertEqual(force_str(style_config["module"]), expected_struct_en["module"])
self.assertEqual(force_str(style_config["name"]), expected_struct_en["name"])
with force_language("de"):
self.assertEqual(force_str(style_config["module"]), expected_struct_de["module"])
self.assertEqual(force_str(style_config["name"]), expected_struct_de["name"])
def test_plugin_toolbar_struct_permissions(self):
page = self.get_permissions_test_page()
page_content = self.get_pagecontent_obj(page)
page_edit_url = get_object_edit_url(page_content)
staff_user = self.get_staff_user_with_no_permissions()
placeholder = page.get_placeholders("en").get(slot="body")
self.add_permission(staff_user, "change_page")
self.add_permission(staff_user, "add_text")
with self.login_user_context(staff_user):
request = self.get_request(page_edit_url, page=page)
request.toolbar = CMSToolbar(request)
renderer = self.get_structure_renderer(request=request)
output = renderer.render_placeholder(placeholder, language="en")
self.assertIn('Text', output)
self.assertNotIn('Link', output)
def test_plugin_child_classes_from_settings(self):
page = api.create_page("page", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").get(slot="body")
ChildClassesPlugin = type(
"ChildClassesPlugin",
(CMSPluginBase,),
dict(child_classes=["TextPlugin"], render_template="allow_children_plugin.html"),
)
with register_plugins(ChildClassesPlugin):
plugin = api.add_plugin(placeholder, ChildClassesPlugin, settings.LANGUAGES[0][0])
plugin = plugin.get_plugin_class_instance()
# assert baseline
self.assertEqual(["TextPlugin"], plugin.get_child_classes(placeholder.slot, placeholder.source))
CMS_PLACEHOLDER_CONF = {
"body": {
"child_classes": {
# Config includes existing LinkPlugin and non-existing PicturePlugin
"ChildClassesPlugin": ["LinkPlugin", "PicturePlugin"],
}
}
}
with override_placeholder_conf(CMS_PLACEHOLDER_CONF=CMS_PLACEHOLDER_CONF):
self.assertEqual(["LinkPlugin"], plugin.get_child_classes(placeholder.slot, placeholder.source))
def test_plugin_parent_classes_from_settings(self):
page = api.create_page("page", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").get(slot="body")
ParentClassesPlugin = type(
"ParentClassesPlugin", (CMSPluginBase,), dict(parent_classes=["TextPlugin"], render_plugin=False)
)
with register_plugins(ParentClassesPlugin):
plugin = api.add_plugin(placeholder, ParentClassesPlugin, settings.LANGUAGES[0][0])
plugin = plugin.get_plugin_class_instance()
# assert baseline
self.assertEqual(["TextPlugin"], plugin.get_parent_classes(placeholder.slot, page))
CMS_PLACEHOLDER_CONF = {
"body": {
"parent_classes": {
"ParentClassesPlugin": ["TestPlugin"],
}
}
}
with override_placeholder_conf(CMS_PLACEHOLDER_CONF=CMS_PLACEHOLDER_CONF):
self.assertEqual(["TestPlugin"], plugin.get_parent_classes(placeholder.slot, page))
def test_plugin_parent_classes_from_object(self):
page = api.create_page("page", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").get(slot="body")
ParentPlugin = type("ParentPlugin", (CMSPluginBase,), dict(render_plugin=False, allow_children=True))
ChildPlugin = type("ChildPlugin", (CMSPluginBase,), dict(parent_classes=["ParentPlugin"], render_plugin=False))
with register_plugins(ParentPlugin, ChildPlugin):
plugin = api.add_plugin(placeholder, ParentPlugin, settings.LANGUAGES[0][0])
plugin = plugin.get_plugin_class_instance()
# assert baseline
child_classes = plugin.get_child_classes(placeholder.slot, placeholder.source)
self.assertIn("ChildPlugin", child_classes)
self.assertIn("ParentPlugin", child_classes)
def test_plugin_child_classes_auto(self):
"""``child_classes = "auto"`` accepts exactly those plugins that explicitly name this
plugin in their ``parent_classes`` -- and nothing else."""
page = api.create_page("page", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").get(slot="body")
AutoParentPlugin = type(
"AutoParentPlugin",
(CMSPluginBase,),
dict(child_classes="auto", allow_children=True, render_template="allow_children_plugin.html"),
)
# Opts in by naming AutoParentPlugin as a parent.
OptInChildPlugin = type(
"OptInChildPlugin", (CMSPluginBase,), dict(parent_classes=["AutoParentPlugin"], render_plugin=False)
)
# Names a *different* parent -- must not be auto-allowed.
OtherParentChildPlugin = type(
"OtherParentChildPlugin", (CMSPluginBase,), dict(parent_classes=["SomeOtherPlugin"], render_plugin=False)
)
# No parent restriction at all -- not an explicit opt-in, so must not be auto-allowed.
UnrestrictedChildPlugin = type("UnrestrictedChildPlugin", (CMSPluginBase,), dict(render_plugin=False))
with register_plugins(
AutoParentPlugin, OptInChildPlugin, OtherParentChildPlugin, UnrestrictedChildPlugin
):
plugin = api.add_plugin(placeholder, AutoParentPlugin, settings.LANGUAGES[0][0])
instance = plugin.get_plugin_class_instance()
child_classes = instance.get_child_classes(placeholder.slot, placeholder.source)
# Only the plugin that explicitly names AutoParentPlugin as parent is allowed.
self.assertEqual(["OptInChildPlugin"], child_classes)
def test_plugin_child_classes_auto_no_opt_in(self):
"""``child_classes = "auto"`` with no plugin naming it as parent allows no children
(and notably does not raise on the many installed plugins whose ``parent_classes`` is
``None``)."""
from cms.utils.plugins import get_plugin_restrictions
page = api.create_page("page", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").get(slot="body")
LonelyAutoPlugin = type(
"LonelyAutoPlugin",
(CMSPluginBase,),
dict(child_classes="auto", allow_children=True, render_template="allow_children_plugin.html"),
)
with register_plugins(LonelyAutoPlugin):
plugin = api.add_plugin(placeholder, LonelyAutoPlugin, settings.LANGUAGES[0][0])
instance = plugin.get_plugin_class_instance()
self.assertEqual([], instance.get_child_classes(placeholder.slot, placeholder.source))
# An empty allow-list is normalised to [""] (no children) by the restriction cache.
child_classes, _ = get_plugin_restrictions(plugin, placeholder.source, {})
self.assertEqual([""], child_classes)
def test_resolved_child_classes_addability(self):
"""The resolved ``child_classes`` (stashed on ``CMSPlugin.child_class_restrictions``
and consumed by the drag-item template via ``any(...)`` / ``|join:""``) reflect
whether any plugin can actually be added: falsy for ``allow_children = False`` *and*
for an empty resolved ``child_classes`` (used to grey out the structure-editor
"add plugin" button)."""
from cms.utils.plugins import get_plugin_restrictions
page = api.create_page("page", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").get(slot="body")
NoChildrenPlugin = type("NoChildrenPlugin", (CMSPluginBase,), dict(render_plugin=False))
EmptyChildPlugin = type(
"EmptyChildPlugin",
(CMSPluginBase,),
dict(allow_children=True, child_classes=[], render_template="allow_children_plugin.html"),
)
OpenChildPlugin = type(
"OpenChildPlugin",
(CMSPluginBase,),
dict(allow_children=True, render_template="allow_children_plugin.html"),
)
RestrictedChildPlugin = type(
"RestrictedChildPlugin",
(CMSPluginBase,),
dict(allow_children=True, child_classes=["LinkPlugin"], render_template="allow_children_plugin.html"),
)
RestrictedNoChildPlugin = type(
"RestrictedNoChildPlugin",
(CMSPluginBase,),
dict(allow_children=False, child_classes=["LinkPlugin"], render_template="allow_children_plugin.html"),
)
def allows_add_children(plugin):
child_classes, _ = get_plugin_restrictions(plugin, page=placeholder.source)
return any(child_classes)
with register_plugins(NoChildrenPlugin, EmptyChildPlugin, OpenChildPlugin, RestrictedChildPlugin, RestrictedNoChildPlugin):
lang = settings.LANGUAGES[0][0]
# allow_children = False -> cannot add children
no_children = api.add_plugin(placeholder, NoChildrenPlugin, lang)
self.assertFalse(allows_add_children(no_children))
# allow_children = True but child_classes = [] -> still cannot add children (the bug)
empty = api.add_plugin(placeholder, EmptyChildPlugin, lang)
self.assertFalse(allows_add_children(empty))
# allow_children = True, no restriction -> can add children
open_ = api.add_plugin(placeholder, OpenChildPlugin, lang)
self.assertTrue(allows_add_children(open_))
# allow_children = True with a concrete child class -> can add children
restricted = api.add_plugin(placeholder, RestrictedChildPlugin, lang)
self.assertTrue(allows_add_children(restricted))
# allow_children = False with a concrete child class -> cannot add children
restricted_no = api.add_plugin(placeholder, RestrictedNoChildPlugin, lang)
self.assertFalse(allows_add_children(restricted_no))
def test_plugin_child_classes_cache_ignores_uncachable_children(self):
from cms.utils.plugins import get_plugin_restrictions
page = api.create_page("page", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").get(slot="body")
ParentPlugin = type("ParentPlugin", (CMSPluginBase,), dict(render_plugin=False, allow_children=True))
ChildPlugin = type(
"ChildPlugin",
(CMSPluginBase,),
dict(
cache_parent_classes=False,
parent_classes=["ParentPlugin"],
render_plugin=False,
),
)
restriction_cache = {} # cache is empty
with register_plugins(ParentPlugin, ChildPlugin):
plugin = api.add_plugin(placeholder, ParentPlugin, settings.LANGUAGES[0][0])
# Populate cache
child_classes, _ = get_plugin_restrictions(plugin, placeholder.source, restriction_cache)
# Baseline
self.assertIn("ChildPlugin", child_classes)
self.assertIn("ParentPlugin", child_classes)
# Change parent class rules (cache should NOT be used)
ChildPlugin.parent_classes = [""]
# Use cache
child_classes, _ = get_plugin_restrictions(plugin, placeholder.source, restriction_cache)
# Despite using the cache the change in allowed parent plugins should be reflected
self.assertNotIn("ChildPlugin", child_classes)
self.assertIn("ParentPlugin", child_classes)
def test_plugin_child_classes_allows_add_by_uncachable_children(self):
from cms.utils.plugins import get_plugin_restrictions
page = api.create_page("page", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").get(slot="body")
ParentPlugin = type("ParentPlugin", (CMSPluginBase,), dict(render_plugin=False, allow_children=True))
ChildPlugin = type(
"ChildPlugin",
(CMSPluginBase,),
dict(
cache_parent_classes=False,
parent_classes=[""],
render_plugin=False,
),
)
restriction_cache = {} # cache is empty
with register_plugins(ParentPlugin, ChildPlugin):
plugin = api.add_plugin(placeholder, ParentPlugin, settings.LANGUAGES[0][0])
# Populate cache
child_classes, _ = get_plugin_restrictions(plugin, placeholder.source, restriction_cache)
# Baseline
self.assertNotIn("ChildPlugin", child_classes)
self.assertIn("ParentPlugin", child_classes)
# Change parent class rules (cache should NOT be used)
ChildPlugin.parent_classes = ["ParentPlugin"]
# Use cache
child_classes, _ = get_plugin_restrictions(plugin, placeholder.source, restriction_cache)
# Despite using the cache the change in allowed parent plugins should be reflected
self.assertIn("ChildPlugin", child_classes)
self.assertIn("ParentPlugin", child_classes)
def test_plugin_child_classes_empty_list_replaced_with_none(self):
from cms.utils.plugins import get_plugin_restrictions
page = api.create_page("page", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").get(slot="body")
# Create a plugin that allows children
NoChildPlugin = type("NoChildPlugin", (CMSPluginBase,), dict(render_plugin=False, allow_children=True))
# Set CMS_PLACEHOLDER_CONF to restrict child_classes to a non-existent plugin
CMS_PLACEHOLDER_CONF = {"body": {"child_classes": {"NoChildPlugin": ["NonExistentPlugin"]}}}
with register_plugins(NoChildPlugin):
with override_placeholder_conf(CMS_PLACEHOLDER_CONF=CMS_PLACEHOLDER_CONF):
plugin = api.add_plugin(placeholder, NoChildPlugin, settings.LANGUAGES[0][0])
restriction_cache = {}
# First call populates the cache
get_plugin_restrictions(plugin, placeholder.source, restriction_cache)
# Second call uses the cache, which should have [""] instead of []
child_classes, _ = get_plugin_restrictions(plugin, placeholder.source, restriction_cache)
# Since the restriction results in no valid child classes, it should be replaced with [""]
self.assertEqual(child_classes, [""])
def test_plugin_child_classes_empty_list_allows_no_children(self):
"""An empty ``child_classes`` list means no plugins may be added as children, as
opposed to ``None`` which allows any plugin."""
from cms.utils.plugins import get_plugin_restrictions
page = api.create_page("page", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").get(slot="body")
NoChildPlugin = type(
"NoChildPlugin",
(CMSPluginBase,),
dict(render_plugin=False, allow_children=True, child_classes=[]),
)
with register_plugins(NoChildPlugin):
plugin = api.add_plugin(placeholder, NoChildPlugin, settings.LANGUAGES[0][0])
instance = plugin.get_plugin_class_instance()
# No plugin is an allowed child
self.assertEqual([], instance.get_child_classes(placeholder.slot, placeholder.source))
# The restriction is sent to the frontend as the [""] sentinel (disallow all)
child_classes, _ = get_plugin_restrictions(plugin, placeholder.source, {})
self.assertEqual([""], child_classes)
def test_plugin_parent_classes_empty_list_allows_no_parent(self):
"""An empty ``parent_classes`` list means the plugin allows no parents (and is therefore
never a valid child), as opposed to ``None`` which allows any parent."""
page = api.create_page("page", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").get(slot="body")
ParentPlugin = type("ParentPlugin", (CMSPluginBase,), dict(render_plugin=False, allow_children=True))
OrphanPlugin = type("OrphanPlugin", (CMSPluginBase,), dict(render_plugin=False, parent_classes=[]))
with register_plugins(ParentPlugin, OrphanPlugin):
plugin = api.add_plugin(placeholder, ParentPlugin, settings.LANGUAGES[0][0])
instance = plugin.get_plugin_class_instance()
child_classes = instance.get_child_classes(placeholder.slot, placeholder.source)
# A plugin with an empty parent_classes is never a valid child
self.assertNotIn("OrphanPlugin", child_classes)
# A plugin with parent_classes=None is still allowed as a child
self.assertIn("ParentPlugin", child_classes)
# An empty parent_classes does not require a parent, so it can be a root plugin
self.assertFalse(OrphanPlugin.requires_parent_plugin(placeholder.slot, page))
def test_plugin_child_classes_glob(self):
"""Glob patterns in child_classes are expanded to matching registered plugin names."""
page = api.create_page("page", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").get(slot="body")
GlobChildPlugin = type(
"GlobChildPlugin",
(CMSPluginBase,),
dict(child_classes=["Link*"], render_template="allow_children_plugin.html"),
)
with register_plugins(GlobChildPlugin):
plugin = api.add_plugin(placeholder, GlobChildPlugin, settings.LANGUAGES[0][0])
instance = plugin.get_plugin_class_instance()
child_classes = instance.get_child_classes(placeholder.slot, placeholder.source)
self.assertIn("LinkPlugin", child_classes)
self.assertNotIn("TextPlugin", child_classes)
def test_plugin_child_classes_glob_no_match(self):
"""A child_classes glob matching no plugin means no children are allowed (like ``[]``)."""
from cms.utils.plugins import get_plugin_restrictions
page = api.create_page("page", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").get(slot="body")
GlobChildPlugin = type(
"GlobChildPlugin",
(CMSPluginBase,),
dict(child_classes=["DoesNotExist*"], allow_children=True, render_template="allow_children_plugin.html"),
)
with register_plugins(GlobChildPlugin):
plugin = api.add_plugin(placeholder, GlobChildPlugin, settings.LANGUAGES[0][0])
instance = plugin.get_plugin_class_instance()
self.assertEqual([], instance.get_child_classes(placeholder.slot, placeholder.source))
child_classes, _ = get_plugin_restrictions(plugin, placeholder.source, {})
self.assertEqual([""], child_classes)
def test_plugin_parent_classes_glob(self):
"""Glob patterns in parent_classes are expanded to matching registered plugin names."""
page = api.create_page("page", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").get(slot="body")
ParentPlugin = type("ParentPlugin", (CMSPluginBase,), dict(render_plugin=False, allow_children=True))
ChildPlugin = type("ChildPlugin", (CMSPluginBase,), dict(parent_classes=["Parent*"], render_plugin=False))
with register_plugins(ParentPlugin, ChildPlugin):
plugin = api.add_plugin(placeholder, ParentPlugin, settings.LANGUAGES[0][0])
instance = plugin.get_plugin_class_instance()
# "Parent*" expands to include ParentPlugin, so ChildPlugin is a valid child
self.assertIn("ParentPlugin", ChildPlugin.get_parent_classes(placeholder.slot, page))
self.assertIn("ChildPlugin", instance.get_child_classes(placeholder.slot, placeholder.source))
def test_plugin_parent_classes_star_requires_any_parent(self):
"""``parent_classes = ["*"]`` requires a parent (any plugin), unlike ``None``."""
page = api.create_page("page", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").get(slot="body")
AnyParentPlugin = type("AnyParentPlugin", (CMSPluginBase,), dict(parent_classes=["*"], render_plugin=False))
NoRestrictionPlugin = type("NoRestrictionPlugin", (CMSPluginBase,), dict(render_plugin=False))
with register_plugins(AnyParentPlugin, NoRestrictionPlugin):
# "*" expands to every registered plugin, so a parent is required ...
self.assertTrue(AnyParentPlugin.requires_parent_plugin(placeholder.slot, page))
# ... and any plugin is an acceptable parent.
self.assertIn("AnyParentPlugin", NoRestrictionPlugin.get_child_classes(placeholder.slot, placeholder.source))
# By contrast, parent_classes=None does not require a parent.
self.assertFalse(NoRestrictionPlugin.requires_parent_plugin(placeholder.slot, page))
def test_plugin_require_parent_from_object(self):
page = api.create_page("page", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").get(slot="body")
ParentPlugin = type("ParentPlugin", (CMSPluginBase,), dict(render_plugin=False, allow_children=True))
ChildPlugin = type("ChildPlugin", (CMSPluginBase,), dict(require_parent=True, render_plugin=False))
with register_plugins(ParentPlugin, ChildPlugin):
plugin = api.add_plugin(placeholder, ParentPlugin, settings.LANGUAGES[0][0])
plugin = plugin.get_plugin_class_instance()
# assert baseline
child_classes = plugin.get_child_classes(placeholder.slot, placeholder.source)
self.assertIn("ChildPlugin", child_classes)
self.assertIn("ParentPlugin", child_classes)
def test_plugin_pool_register_returns_plugin_class(self):
@plugin_pool.register_plugin
class DecoratorTestPlugin(CMSPluginBase):
render_plugin = False
name = "Test Plugin"
self.assertIsNotNone(DecoratorTestPlugin)
def _create_plugin_tree(self, placeholder, n, downcast=False):
import random
from cms.utils.plugins import downcast_plugins
with register_plugins(CMSPluginBase):
# Create n plugins, shuffle them and then create a linear tree of them
plugins = random.sample([api.add_plugin(placeholder, CMSPluginBase, "en") for _ in range(n)], k=n)
for i, plugin in enumerate(plugins):
plugin.position = n + i + 1 # order positions, too
if i > 0:
plugin.parent_id = plugins[i - 1].pk
plugin.save()
for i, plugin in enumerate(plugins, start=1):
plugin.position = i # recalculate positions
plugin.save()
if downcast:
plugins = list(downcast_plugins(plugins))
return plugins
def test_get_ancestors(self):
placeholder = self.get_placeholder()
plugins = self._create_plugin_tree(placeholder, 10, downcast=False)
with self.assertNumQueries(2):
ancestors = plugins[-1].get_ancestors()
self.assertEqual([ancestor.pk for ancestor in ancestors], [plugin.pk for plugin in plugins[:-1]])
def test_get_ancestors_no_parent(self):
placeholder = self.get_placeholder()
with register_plugins(CMSPluginBase):
# Create a single plugin with no parent
plugin = api.add_plugin(placeholder, CMSPluginBase, "en")
plugin.save()
with self.assertNumQueries(0):
ancestors = plugin.get_ancestors()
self.assertEqual(ancestors, [])
def test_get_ancestors_with_downcasted_plugins(self):
placeholder = self.get_placeholder()
plugins = self._create_plugin_tree(placeholder, 10, downcast=True)
with self.assertNumQueries(0):
ancestors = plugins[-1].get_ancestors()
self.assertEqual([ancestor.pk for ancestor in ancestors], [plugin.pk for plugin in plugins[:-1]])
def test_iterator_chunk_size_in_get_bound_plugins(self):
"""iterator() must pass PLUGIN_ITERATOR_CHUNK_SIZE so that
querysets with prefetch_related (from custom managers) don't
crash on Django 5.0+."""
from cms.constants import PLUGIN_ITERATOR_CHUNK_SIZE
from cms.utils.plugins import get_bound_plugins
placeholder = self.get_placeholder()
with register_plugins(DumbFixturePlugin):
plugin = api.add_plugin(placeholder, DumbFixturePlugin, "en")
plugins = CMSPlugin.objects.filter(pk=plugin.pk)
with mock.patch(
"cms.utils.plugins.PLUGIN_ITERATOR_CHUNK_SIZE", 999
):
# Patch the queryset's iterator to inspect the chunk_size kwarg
original_iterator = None
def patched_iterator(chunk_size=None):
self.assertEqual(chunk_size, 999)
return original_iterator(chunk_size=chunk_size)
qs = DumbFixturePlugin.model.objects.filter(pk__in=[plugin.pk])
original_iterator = qs.iterator
with mock.patch.object(
DumbFixturePlugin.model.objects, "filter", return_value=qs
):
with mock.patch.object(qs, "iterator", side_effect=patched_iterator):
list(get_bound_plugins(plugins))
def test_downcast_plugins_with_direct_cmsplugin_proxy(self):
from cms.utils.plugins import downcast_plugins
placeholder = self.get_placeholder()
with register_plugins(DirectProxyCMSPlugin):
plugin = api.add_plugin(placeholder, DirectProxyCMSPlugin, "en")
cms_plugin = CMSPlugin.objects.get(pk=plugin.pk)
downcasted = list(downcast_plugins([cms_plugin]))
self.assertEqual(len(downcasted), 1)
self.assertIsInstance(downcasted[0], DirectProxyCMSPluginModel)
self.assertEqual(downcasted[0].pk, plugin.pk)
def test_get_bound_plugins_with_direct_cmsplugin_proxy(self):
from cms.utils.plugins import get_bound_plugins
placeholder = self.get_placeholder()
with register_plugins(DirectProxyCMSPlugin):
plugin = api.add_plugin(placeholder, DirectProxyCMSPlugin, "en")
cms_plugin = CMSPlugin.objects.get(pk=plugin.pk)
bound_plugins = list(get_bound_plugins([cms_plugin]))
self.assertEqual(len(bound_plugins), 1)
self.assertIsInstance(bound_plugins[0], DirectProxyCMSPluginModel)
self.assertEqual(bound_plugins[0].pk, plugin.pk)
class PluginManyToManyTestCase(PluginsTestBaseCase):
def setUp(self):
self.super_user = self._create_user("test", True, True)
self.slave = self._create_user("slave", True)
self._login_context = self.login_user_context(self.super_user)
self._login_context.__enter__()
# create 3 sections
self.sections = []
self.section_pks = []
for i in range(3):
section = Section.objects.create(name="section %s" % i)
self.sections.append(section)
self.section_pks.append(section.pk)
self.section_count = len(self.sections)
# create 10 articles by section
for section in self.sections:
for j in range(10):
Article.objects.create(title="article %s" % j, section=section)
self.FIRST_LANG = settings.LANGUAGES[0][0]
self.SECOND_LANG = settings.LANGUAGES[1][0]
def test_dynamic_plugin_template(self):
page_en = api.create_page("CopyPluginTestPage (EN)", "nav_playground.html", "en")
ph_en = page_en.get_placeholders("en").get(slot="body")
api.add_plugin(ph_en, "ArticleDynamicTemplatePlugin", "en", title="a title")
api.add_plugin(ph_en, "ArticleDynamicTemplatePlugin", "en", title="custom template")
context = self.get_context(path=page_en.get_absolute_url())
request = context["request"]
plugins = get_plugins(request, ph_en, page_en.template)
content_renderer = self.get_content_renderer()
for plugin in plugins:
if plugin.title == "custom template":
content = content_renderer.render_plugin(plugin, context, ph_en)
self.assertEqual(
plugin.get_plugin_class_instance().get_render_template({}, plugin, ph_en), "articles_custom.html"
)
self.assertTrue("Articles Custom template" in content)
else:
content = content_renderer.render_plugin(plugin, context, ph_en)
self.assertEqual(
plugin.get_plugin_class_instance().get_render_template({}, plugin, ph_en), "articles.html"
)
self.assertFalse("Articles Custom template" in content)
def test_add_plugin_with_m2m(self):
# add a new text plugin
self.assertEqual(ArticlePluginModel.objects.count(), 0)
page_data = self.get_new_page_data()
self.client.post(self.get_page_add_uri("en"), page_data)
page = Page.objects.first()
placeholder = page.get_placeholders(self.FIRST_LANG).get(slot="col_left")
add_url = self.get_add_plugin_uri(
placeholder=placeholder,
plugin_type="ArticlePlugin",
language=self.FIRST_LANG,
)
data = {"title": "Articles Plugin 1", "sections": self.section_pks}
response = self.client.post(add_url, data)
self.assertEqual(response.status_code, 200)
self.assertEqual(ArticlePluginModel.objects.count(), 1)
plugin = ArticlePluginModel.objects.all()[0]
self.assertEqual(self.section_count, plugin.sections.count())
response = self.client.get("/en/")
self.assertEqual(response.status_code, 200)
self.assertEqual(plugin.sections.through._meta.db_table, "manytomany_rel_articlepluginmodel_sections")
def test_copy_plugin_with_m2m(self):
page = api.create_page("page", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").get(slot="body")
plugin = ArticlePluginModel.objects.create(
plugin_type="ArticlePlugin",
placeholder=placeholder,
position=1,
language=self.FIRST_LANG,
)
endpoint = self.get_change_plugin_uri(plugin, language=self.FIRST_LANG)
data = {"title": "Articles Plugin 1", "sections": self.section_pks}
response = self.client.post(endpoint, data)
self.assertEqual(response.status_code, 200)
self.assertEqual(ArticlePluginModel.objects.count(), 1)
self.assertEqual(ArticlePluginModel.objects.all()[0].sections.count(), self.section_count)
page_data = self.get_new_page_data()
# create 2nd language page
page_data.update(
{
"title": f"{page.get_title()} {self.SECOND_LANG}",
"cms_page": page.pk,
}
)
endpoint = self.get_page_add_uri(self.SECOND_LANG, page)
response = self.client.post(endpoint, page_data)
self.assertRedirects(response, self.get_pages_admin_list_uri(self.SECOND_LANG))
self.assertEqual(CMSPlugin.objects.filter(language=self.FIRST_LANG).count(), 1)
self.assertEqual(CMSPlugin.objects.filter(language=self.SECOND_LANG).count(), 0)
self.assertEqual(CMSPlugin.objects.count(), 1)
self.assertEqual(Page.objects.all().count(), 1)
copy_data = {
"source_placeholder_id": placeholder.pk,
"target_placeholder_id": placeholder.pk,
"target_language": self.SECOND_LANG,
"source_language": self.FIRST_LANG,
}
endpoint = self.get_copy_plugin_uri(plugin, language=self.FIRST_LANG)
response = self.client.post(endpoint, copy_data)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content.decode("utf8").count('"plugin_type": "ArticlePlugin"'), 1)
# assert copy success
self.assertEqual(CMSPlugin.objects.filter(language=self.FIRST_LANG).count(), 1)
self.assertEqual(CMSPlugin.objects.filter(language=self.SECOND_LANG).count(), 1)
self.assertEqual(CMSPlugin.objects.count(), 2)
db_counts = [plgn.sections.count() for plgn in ArticlePluginModel.objects.all()]
expected = [self.section_count for _ in range(len(db_counts))]
self.assertEqual(expected, db_counts)
class PluginCopyRelationsTestCase(PluginsTestBaseCase):
"""Test the suggestions in the docs for copy_relations()"""
def setUp(self):
self.super_user = self._create_user("test", True, True)
self.FIRST_LANG = settings.LANGUAGES[0][0]
self._login_context = self.login_user_context(self.super_user)
self._login_context.__enter__()
page_data1 = self.get_new_page_data_dbfields()
page_data1["published"] = False
self.page1 = api.create_page(**page_data1)
page_data2 = self.get_new_page_data_dbfields()
page_data2["published"] = False
self.page2 = api.create_page(**page_data2)
self.placeholder1 = self.page1.get_placeholders(self.FIRST_LANG).get(slot="body")
self.placeholder2 = self.page2.get_placeholders(self.FIRST_LANG).get(slot="body")
class PluginsMetaOptionsTests(TestCase):
"""TestCase set for ensuring that bugs like #992 are caught"""
# these plugins are inlined because, due to the nature of the #992
# ticket, we cannot actually import a single file with all the
# plugin variants in, because that calls __new__, at which point the
# error with split occurs.
def test_meta_options_as_defaults(self):
"""handling when a CMSPlugin meta options are computed defaults"""
# this plugin relies on the base CMSPlugin and Model classes to
# decide what the app_label and db_table should be
plugin = TestPlugin.model
self.assertEqual(plugin._meta.db_table, "meta_testpluginmodel")
self.assertEqual(plugin._meta.app_label, "meta")
def test_meta_options_as_declared_defaults(self):
"""handling when a CMSPlugin meta options are declared as per defaults"""
# here, we declare the db_table and app_label explicitly, but to the same
# values as would be computed, thus making sure it's not a problem to
# supply options.
plugin = TestPlugin2.model
self.assertEqual(plugin._meta.db_table, "meta_testpluginmodel2")
self.assertEqual(plugin._meta.app_label, "meta")
def test_meta_options_custom_app_label(self):
"""make sure customised meta options on CMSPlugins don't break things"""
plugin = TestPlugin3.model
self.assertEqual(plugin._meta.db_table, "one_thing_testpluginmodel3")
self.assertEqual(plugin._meta.app_label, "one_thing")
def test_meta_options_custom_db_table(self):
"""make sure custom database table names are OK."""
plugin = TestPlugin4.model
self.assertEqual(plugin._meta.db_table, "or_another_4")
self.assertEqual(plugin._meta.app_label, "meta")
def test_meta_options_custom_both(self):
"""We should be able to customise app_label and db_table together"""
plugin = TestPlugin5.model
self.assertEqual(plugin._meta.db_table, "or_another_5")
self.assertEqual(plugin._meta.app_label, "one_thing")
class NoDatabasePluginTests(TestCase):
def get_plugin_model(self, plugin_type):
return plugin_pool.get_plugin(plugin_type).model
def test_render_meta_is_unique(self):
text = self.get_plugin_model("TextPlugin")
link = self.get_plugin_model("LinkPlugin")
self.assertNotEqual(id(text._render_meta), id(link._render_meta))
def test_render_meta_does_not_leak(self):
text = self.get_plugin_model("TextPlugin")
link = self.get_plugin_model("LinkPlugin")
text._render_meta.text_enabled = False
link._render_meta.text_enabled = False
self.assertFalse(text._render_meta.text_enabled)
self.assertFalse(link._render_meta.text_enabled)
link._render_meta.text_enabled = True
self.assertFalse(text._render_meta.text_enabled)
self.assertTrue(link._render_meta.text_enabled)
def test_db_table_hack(self):
# Plugin models have been moved away due to Django's AppConfig
from cms.test_utils.project.bunch_of_plugins.models import TestPlugin1
self.assertEqual(TestPlugin1._meta.db_table, "bunch_of_plugins_testplugin1")
def test_db_table_hack_with_mixin(self):
# Plugin models have been moved away due to Django's AppConfig
from cms.test_utils.project.bunch_of_plugins.models import TestPlugin2
self.assertEqual(TestPlugin2._meta.db_table, "bunch_of_plugins_testplugin2")
class SimplePluginTests(TestCase):
def test_simple_naming(self):
class MyPlugin(CMSPluginBase):
render_template = "base.html"
self.assertEqual(MyPlugin.name, "My Plugin")
def test_simple_context(self):
class MyPlugin(CMSPluginBase):
render_template = "base.html"
plugin = MyPlugin(ArticlePluginModel, admin.site)
context = {}
out_context = plugin.render(context, 1, 2)
self.assertEqual(out_context["instance"], 1)
self.assertEqual(out_context["placeholder"], 2)
self.assertIs(out_context, context)
def test_allowed_models_is_lowercased_for_list(self):
class MyPlugin(CMSPluginBase):
render_template = "base.html"
allowed_models = ["Cms.PageContent", "MyApp.MyModel"]
self.assertEqual(MyPlugin.allowed_models, ["cms.pagecontent", "myapp.mymodel"])
self.assertIsInstance(MyPlugin.allowed_models, list)
def test_allowed_models_single_string_is_coerced_and_lowercased(self):
class SinglePlugin(CMSPluginBase):
render_template = "base.html"
allowed_models = "Cms.PageContent"
self.assertEqual(SinglePlugin.allowed_models, ["cms.pagecontent"])
self.assertIsInstance(SinglePlugin.allowed_models, list)
class BrokenPluginTests(TestCase):
def test_import_broken_plugin(self):
"""
If there is an import error in the actual cms_plugin file it should
raise the ImportError rather than silently swallowing it -
in opposition to the ImportError if the file 'cms_plugins.py' doesn't
exist.
"""
new_apps = ["cms.test_utils.project.brokenpluginapp"]
with self.settings(INSTALLED_APPS=new_apps):
plugin_pool.discovered = False
self.assertRaises(ImportError, plugin_pool.discover_plugins)
class MTIPluginsTestCase(PluginsTestBaseCase):
def test_add_edit_plugin(self):
from cms.test_utils.project.mti_pluginapp.models import TestPluginBetaModel
"""
Test that we can instantiate and use a MTI plugin
"""
# Create a page
page = create_page("Test", "nav_playground.html", settings.LANGUAGES[0][0])
placeholder = page.get_placeholders(settings.LANGUAGES[0][0]).get(slot="body")
# Add the MTI plugin
add_url = self.get_add_plugin_uri(
placeholder=placeholder,
plugin_type="TestPluginBeta",
language=settings.LANGUAGES[0][0],
)
data = {"alpha": "ALPHA", "beta": "BETA"}
response = self.client.post(add_url, data)
self.assertEqual(response.status_code, 200)
self.assertEqual(TestPluginBetaModel.objects.count(), 1)
plugin_model = TestPluginBetaModel.objects.all()[0]
self.assertEqual("ALPHA", plugin_model.alpha)
self.assertEqual("BETA", plugin_model.beta)
def test_related_name(self):
from cms.test_utils.project.mti_pluginapp.models import (
AbstractPluginParent,
LessMixedPlugin,
MixedPlugin,
NonPluginModel,
ProxiedAlphaPluginModel,
ProxiedBetaPluginModel,
TestPluginAlphaModel,
TestPluginBetaModel,
TestPluginGammaModel,
)
# the first concrete class of the following four plugins is TestPluginAlphaModel
self.assertEqual(
TestPluginAlphaModel.cmsplugin_ptr.field.remote_field.related_name, "mti_pluginapp_testpluginalphamodel"
)
self.assertEqual(
TestPluginBetaModel.cmsplugin_ptr.field.remote_field.related_name, "mti_pluginapp_testpluginalphamodel"
)
self.assertEqual(
ProxiedAlphaPluginModel.cmsplugin_ptr.field.remote_field.related_name, "mti_pluginapp_testpluginalphamodel"
)
self.assertEqual(
ProxiedBetaPluginModel.cmsplugin_ptr.field.remote_field.related_name, "mti_pluginapp_testpluginalphamodel"
)
# Abstract plugins will have the dynamic format for related name
self.assertEqual(AbstractPluginParent.cmsplugin_ptr.field.remote_field.related_name, "%(app_label)s_%(class)s")
# Concrete plugin of an abstract plugin gets its relatedname
self.assertEqual(
TestPluginGammaModel.cmsplugin_ptr.field.remote_field.related_name, "mti_pluginapp_testplugingammamodel"
)
# Child plugin gets it's own related name
self.assertEqual(MixedPlugin.cmsplugin_ptr.field.remote_field.related_name, "mti_pluginapp_mixedplugin")
# If the child plugin inherit straight from CMSPlugin, even if composed with
# other models, gets its own related_name
self.assertEqual(
LessMixedPlugin.cmsplugin_ptr.field.remote_field.related_name, "mti_pluginapp_lessmixedplugin"
)
# Non plugins are skipped
self.assertFalse(hasattr(NonPluginModel, "cmsplugin_ptr"))
class UserInputValidationPluginTest(PluginsTestBaseCase):
def test_error_response_escapes(self):
language = "en"
superuser = self.get_superuser()
page = create_page("error page", "nav_playground.html", language=language)
placeholder = page.get_placeholders(language).get(slot="body")
add_url = self.get_add_plugin_uri(
placeholder, plugin_type='TextPlugin">', language=language
)
with self.login_user_context(superuser):
response = self.client.get(add_url)
self.assertEqual(response.status_code, 400)
self.assertIn(
"TextPlugin"><script>alert("hello world")</script>",
response.content.decode("utf-8"),
)
class PluginPoolTestCase(CMSTestCase):
"""Test cases for PluginPool methods"""
def setUp(self):
super().setUp()
plugin_pool._clear_cached()
def tearDown(self):
plugin_pool._clear_cached()
super().tearDown()
def test_plugin_pool_clear(self):
"""Test that PluginPool.clear() resets the pool state"""
# Save current pool state
original_plugins = dict(plugin_pool.plugins)
original_discovered = plugin_pool.discovered
try:
# Ensure discovery has happened
plugin_pool.discover_plugins()
# Register a test plugin
@plugin_pool.register_plugin
class TestClearPlugin(CMSPluginBase):
render_plugin = False
name = "Test Clear Plugin"
# Verify plugin is registered
self.assertIn("TestClearPlugin", plugin_pool.plugins)
self.assertTrue(plugin_pool.discovered)
# Clear the pool
plugin_pool.clear()
# Verify pool is reset
self.assertNotIn("TestClearPlugin", plugin_pool.plugins)
self.assertFalse(plugin_pool.discovered)
self.assertEqual(plugin_pool.root_plugin_cache, {})
finally:
# Restore the original pool state
plugin_pool.plugins = original_plugins
plugin_pool.discovered = original_discovered
plugin_pool._clear_cached()
def test_plugin_pool_clear_clears_cached_properties(self):
"""Test that clear() removes cached properties"""
# Save current pool state
original_plugins = dict(plugin_pool.plugins)
original_discovered = plugin_pool.discovered
try:
# Access cached properties to ensure they're created
_ = plugin_pool.registered_plugins
_ = plugin_pool.plugins_with_extra_menu
_ = plugin_pool.plugins_with_extra_placeholder_menu
# Verify cached properties exist
self.assertIn("registered_plugins", plugin_pool.__dict__)
self.assertIn("plugins_with_extra_menu", plugin_pool.__dict__)
self.assertIn("plugins_with_extra_placeholder_menu", plugin_pool.__dict__)
# Clear the pool
plugin_pool.clear()
# Verify cached properties are removed
self.assertNotIn("registered_plugins", plugin_pool.__dict__)
self.assertNotIn("plugins_with_extra_menu", plugin_pool.__dict__)
self.assertNotIn("plugins_with_extra_placeholder_menu", plugin_pool.__dict__)
finally:
# Restore the original pool state
plugin_pool.plugins = original_plugins
plugin_pool.discovered = original_discovered
plugin_pool._clear_cached()
def test_get_restrictions_cache_with_global_cache(self):
"""Test get_restrictions_cache returns global cache for cacheable plugins"""
from cms.plugin_base import template_slot_caching
@plugin_pool.register_plugin
class GlobalCachePlugin(CMSPluginBase):
render_plugin = False
name = "Global Cache Plugin"
@template_slot_caching
def get_require_parent(self, slot, page):
return False
@template_slot_caching
def get_child_class_overrides(self, slot, page):
return []
@template_slot_caching
def get_parent_classes(self, slot, page):
return None
try:
page = create_page("Test", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").first()
plugin_instance = api.add_plugin(placeholder, GlobalCachePlugin, "en")
request_cache = {}
restrictions_cache = plugin_pool.get_restrictions_cache(
request_cache, plugin_instance, page
)
# Should return global cache, not request cache
self.assertIsNot(restrictions_cache, request_cache)
# Global cache should be a defaultdict
self.assertIsInstance(restrictions_cache, dict)
finally:
plugin_pool.unregister_plugin(GlobalCachePlugin)
def test_get_restrictions_cache_with_local_cache(self):
"""Test get_restrictions_cache returns request cache for non-cacheable plugins"""
@plugin_pool.register_plugin
class LocalCachePlugin(CMSPluginBase):
render_plugin = False
name = "Local Cache Plugin"
# Not decorated with @template_slot_caching
def get_require_parent(self, slot, page):
return False
try:
page = create_page("Test", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").first()
plugin_instance = api.add_plugin(placeholder, LocalCachePlugin, "en")
request_cache = {}
restrictions_cache = plugin_pool.get_restrictions_cache(
request_cache, plugin_instance, page
)
# Should return request cache since plugin cannot be cached globally
self.assertIs(restrictions_cache, request_cache)
finally:
plugin_pool.unregister_plugin(LocalCachePlugin)
def test_get_restrictions_cache_get_child_classes_without_decorator(self):
"""A plugin that overrides get_child_classes *without* @template_slot_caching must not be
cached globally, even when all other restriction methods are template/slot-pure.
get_child_classes is part of restriction_methods, so an undecorated override means the
result may depend on more than template+slot and must fall back to the per-request cache.
"""
@plugin_pool.register_plugin
class UncachedChildClassesPlugin(CMSPluginBase):
render_plugin = False
name = "Uncached Child Classes Plugin"
# get_require_parent / get_child_class_overrides / get_parent_classes are inherited
# from CMSPluginBase and *are* decorated. Only get_child_classes loses the decorator.
def get_child_classes(self, slot, page=None, instance=None, only_uncached=False):
return []
try:
self.assertFalse(plugin_pool.can_cache_globally(UncachedChildClassesPlugin))
page = create_page("Test", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").first()
plugin_instance = api.add_plugin(placeholder, UncachedChildClassesPlugin, "en")
request_cache = {}
restrictions_cache = plugin_pool.get_restrictions_cache(
request_cache, plugin_instance, page
)
# Falls back to the request-local cache, not the global cache.
self.assertIs(restrictions_cache, request_cache)
finally:
plugin_pool.unregister_plugin(UncachedChildClassesPlugin)
def test_get_restrictions_cache_inherited_child_classes_is_global(self):
"""The inherited (decorated) get_child_classes keeps a plugin globally cacheable."""
from cms.plugin_base import template_slot_caching
@plugin_pool.register_plugin
class InheritedChildClassesPlugin(CMSPluginBase):
render_plugin = False
name = "Inherited Child Classes Plugin"
@template_slot_caching
def get_require_parent(self, slot, page):
return False
@template_slot_caching
def get_child_class_overrides(self, slot, page):
return []
@template_slot_caching
def get_parent_classes(self, slot, page):
return None
# get_child_classes is inherited from CMSPluginBase, which carries the decorator.
try:
self.assertTrue(plugin_pool.can_cache_globally(InheritedChildClassesPlugin))
page = create_page("Test", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").first()
plugin_instance = api.add_plugin(placeholder, InheritedChildClassesPlugin, "en")
request_cache = {}
restrictions_cache = plugin_pool.get_restrictions_cache(
request_cache, plugin_instance, page
)
# Globally cacheable plugins use the shared global cache, not the request cache.
self.assertIsNot(restrictions_cache, request_cache)
finally:
plugin_pool.unregister_plugin(InheritedChildClassesPlugin)
def test_get_restrictions_cache_different_templates(self):
"""Test get_restrictions_cache with different templates"""
from cms.plugin_base import template_slot_caching
@plugin_pool.register_plugin
class TemplateCachePlugin(CMSPluginBase):
render_plugin = False
name = "Template Cache Plugin"
@template_slot_caching
def get_require_parent(self, slot, page):
return False
@template_slot_caching
def get_child_class_overrides(self, slot, page):
return []
@template_slot_caching
def get_parent_classes(self, slot, page):
return None
try:
# Mark that template restrictions exist
with mock.patch.object(plugin_pool.__class__, "global_template_restrictions", return_value=True):
page1 = create_page("Test1", "nav_playground.html", "en")
page2 = create_page("Test2", "simple.html", "en", parent=page1)
placeholder1 = page1.get_placeholders("en").first()
placeholder2 = page2.get_placeholders("en").first()
plugin_instance1 = api.add_plugin(placeholder1, TemplateCachePlugin, "en")
plugin_instance2 = api.add_plugin(placeholder2, TemplateCachePlugin, "en")
request_cache = {}
cache1 = plugin_pool.get_restrictions_cache(request_cache, plugin_instance1, page1)
cache2 = plugin_pool.get_restrictions_cache(request_cache, plugin_instance2, page2)
# Different templates should potentially use different caches
# (depending on configuration)
self.assertIsInstance(cache1, dict)
self.assertIsInstance(cache2, dict)
finally:
plugin_pool.unregister_plugin(TemplateCachePlugin)
def test_get_restrictions_cache_same_slot_different_objects(self):
"""Test get_restrictions_cache with same slot but different objects"""
from cms.plugin_base import template_slot_caching
from cms.test_utils.project.placeholderapp.models import Example1
@plugin_pool.register_plugin
class ObjectCachePlugin(CMSPluginBase):
render_plugin = False
name = "Object Cache Plugin"
@template_slot_caching
def get_require_parent(self, slot, page):
return False
@template_slot_caching
def get_child_class_overrides(self, slot, page):
return []
@template_slot_caching
def get_parent_classes(self, slot, page):
return None
try:
page = create_page("Test", "nav_playground.html", "en")
example = Example1.objects.create(char_1="test", char_2="test")
page_placeholder = page.get_placeholders("en").first()
example_placeholder = example.placeholder
page_plugin = api.add_plugin(page_placeholder, ObjectCachePlugin, "en")
example_plugin = api.add_plugin(example_placeholder, ObjectCachePlugin, "en")
request_cache = {}
page_cache = plugin_pool.get_restrictions_cache(request_cache, page_plugin, page)
example_cache = plugin_pool.get_restrictions_cache(request_cache, example_plugin, example)
# Different object types should use different caches
self.assertIsInstance(page_cache, dict)
self.assertIsInstance(example_cache, dict)
finally:
plugin_pool.unregister_plugin(ObjectCachePlugin)
class IsSlotPluginTestCase(PluginsTestBaseCase):
"""Unit and integration tests for the is_slot plugin attribute."""
def test_is_slot_default_is_false(self):
"""Unit test: is_slot defaults to False on CMSPluginBase."""
self.assertFalse(CMSPluginBase.is_slot)
def test_is_slot_attribute_on_custom_plugin(self):
"""Unit test: is_slot can be set to True on a plugin class."""
SlotPlugin = type(
"SlotPlugin",
(CMSPluginBase,),
dict(render_plugin=False, is_slot=True),
)
self.assertTrue(SlotPlugin.is_slot)
def test_get_plugin_info_parents_argument_is_deprecated(self):
"""The deprecated ``parents`` argument still works (with a warning) for backwards compat."""
from cms.utils.compat.warnings import RemovedInDjangoCMS60Warning
DeprecationInfoPlugin = type("DeprecationInfoPlugin", (CMSPluginBase,), dict(render_plugin=False))
with register_plugins(DeprecationInfoPlugin):
page = api.create_page("test", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").get(slot="body")
plugin = api.add_plugin(placeholder, DeprecationInfoPlugin, "en")
# Default: no warning, and no parent restriction key in the payload.
with warnings.catch_warnings():
warnings.simplefilter("error")
info = plugin.get_plugin_info()
self.assertNotIn("plugin_parent_restriction", info)
# Deprecated argument: warns but still emits the key for compatibility.
with self.assertWarns(RemovedInDjangoCMS60Warning):
info = plugin.get_plugin_info(parents=["OtherPlugin"])
self.assertEqual(["OtherPlugin"], info["plugin_parent_restriction"])
def test_is_slot_in_plugin_info(self):
"""Unit test: get_plugin_info includes is_slot value."""
SlotPlugin = type(
"SlotPlugin",
(CMSPluginBase,),
dict(render_plugin=False, is_slot=True),
)
NonSlotPlugin = type(
"NonSlotPlugin",
(CMSPluginBase,),
dict(render_plugin=False, is_slot=False),
)
with register_plugins(SlotPlugin, NonSlotPlugin):
page = api.create_page("test", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").get(slot="body")
slot_plugin = api.add_plugin(placeholder, SlotPlugin, "en")
non_slot_plugin = api.add_plugin(placeholder, NonSlotPlugin, "en")
slot_info = slot_plugin.get_plugin_info()
non_slot_info = non_slot_plugin.get_plugin_info()
self.assertTrue(slot_info["is_slot"])
self.assertFalse(non_slot_info["is_slot"])
def test_is_slot_disables_add_form_in_toolbar_struct(self):
"""Unit test: get_toolbar_plugin_struct disables add_form for slot plugins."""
from cms.utils.placeholder import get_toolbar_plugin_struct
SlotPlugin = type(
"SlotPlugin",
(CMSPluginBase,),
dict(render_plugin=False, is_slot=True, show_add_form=True),
)
NonSlotPlugin = type(
"NonSlotPlugin",
(CMSPluginBase,),
dict(render_plugin=False, is_slot=False, show_add_form=True),
)
with register_plugins(SlotPlugin, NonSlotPlugin):
toolbar_struct = get_toolbar_plugin_struct([SlotPlugin, NonSlotPlugin])
slot_config = next(config for config in toolbar_struct if config["value"] == "SlotPlugin")
non_slot_config = next(config for config in toolbar_struct if config["value"] == "NonSlotPlugin")
self.assertFalse(slot_config["add_form"])
self.assertTrue(non_slot_config["add_form"])
def test_is_slot_content_renderer(self):
"""Integration test: content renderer adds cms-slot class when is_slot is True."""
from django.template import RequestContext
SlotPlugin = type(
"SlotPlugin",
(CMSPluginBase,),
dict(render_template="extra_context_plugin.html", is_slot=True),
)
NonSlotPlugin = type(
"NonSlotPlugin",
(CMSPluginBase,),
dict(render_template="extra_context_plugin.html", is_slot=False),
)
with register_plugins(SlotPlugin, NonSlotPlugin):
page = api.create_page("test", "nav_playground.html", "en")
page_content = self.get_pagecontent_obj(page)
page_edit_url = get_object_edit_url(page_content)
placeholder = page.get_placeholders("en").get(slot="body")
slot_instance = api.add_plugin(placeholder, SlotPlugin, "en")
non_slot_instance = api.add_plugin(placeholder, NonSlotPlugin, "en")
request = self.get_request(page_edit_url, page=page)
request.toolbar = CMSToolbar(request)
renderer = self.get_content_renderer(request=request)
context = RequestContext(request)
slot_output = renderer.render_plugin(slot_instance, context, placeholder, editable=True)
non_slot_output = renderer.render_plugin(non_slot_instance, context, placeholder, editable=True)
self.assertIn("cms-slot", slot_output)
self.assertNotIn("cms-slot", non_slot_output)
def test_is_slot_structure_renderer(self):
"""Integration test: structure renderer includes is_slot in plugin toolbar JS."""
SlotPlugin = type(
"SlotPlugin",
(CMSPluginBase,),
dict(render_plugin=False, is_slot=True),
)
with register_plugins(SlotPlugin):
page = api.create_page("test", "nav_playground.html", "en")
page_content = self.get_pagecontent_obj(page)
page_edit_url = get_object_edit_url(page_content)
placeholder = page.get_placeholders("en").get(slot="body")
api.add_plugin(placeholder, SlotPlugin, "en")
request = self.get_request(page_edit_url, page=page)
request.toolbar = CMSToolbar(request)
renderer = self.get_structure_renderer(request=request)
output = renderer.render_placeholder(placeholder, language="en", obj=page)
self.assertIn('"is_slot": true', output)
def test_is_slot_via_plugin_pool(self):
"""Unit test: is_slot can be set on a third-party plugin via plugin_pool.get_plugin()."""
ThirdPartyPlugin = type(
"ThirdPartyPlugin",
(CMSPluginBase,),
dict(render_plugin=False, is_slot=False),
)
with register_plugins(ThirdPartyPlugin):
self.assertFalse(plugin_pool.get_plugin("ThirdPartyPlugin").is_slot)
# Simulate what an AppConfig.ready() method would do
plugin_pool.get_plugin("ThirdPartyPlugin").is_slot = True
self.assertTrue(plugin_pool.get_plugin("ThirdPartyPlugin").is_slot)
# Verify it affects plugin info
page = api.create_page("test", "nav_playground.html", "en")
placeholder = page.get_placeholders("en").get(slot="body")
plugin = api.add_plugin(placeholder, ThirdPartyPlugin, "en")
info = plugin.get_plugin_info()
self.assertTrue(info["is_slot"])