forked from ungleich-public/dynamicweb
2294 lines
103 KiB
Python
2294 lines
103 KiB
Python
|
|
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, '<div class="success"></div>')
|
||
|
|
# 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(), "<Empty>")
|
||
|
|
|
||
|
|
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('<a data-rel="add" data-add-form="true" href="TextPlugin">Text</a>', output)
|
||
|
|
self.assertNotIn('<a data-rel="add" data-add-form="true" href="LinkPlugin">Link</a>', 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"><script>alert("hello world")</script>', 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"])
|