dynamicweb/cms/tests/test_placeholder.py

1811 lines
78 KiB
Python

import json
import re
import warnings
from unittest.mock import patch
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.sites.models import Site
from django.core.exceptions import ImproperlyConfigured
from django.template import Template, TemplateSyntaxError, engines
from django.template.loader import get_template
from django.test import TestCase
from django.test.utils import override_settings
from django.utils.encoding import force_str
from django.utils.numberformat import format
from sekizai.context import SekizaiContext
from cms import constants
from cms.api import add_plugin, create_page, create_page_content
from cms.exceptions import DuplicatePlaceholderWarning, PlaceholderNotFound
from cms.models.placeholdermodel import Placeholder
from cms.models.pluginmodel import CMSPlugin
from cms.models.settingmodels import UserSettings
from cms.plugin_pool import plugin_pool
from cms.test_utils.project.placeholder_relation_field_app.models import (
FancyPoll,
)
from cms.test_utils.project.placeholderapp.models import (
DynamicPlaceholderSlotExample,
Example1,
TwoPlaceholderExample,
)
from cms.test_utils.project.sampleapp.models import Category
from cms.test_utils.testcases import CMSTestCase, TransactionCMSTestCase
from cms.test_utils.util.context_managers import override_placeholder_conf
from cms.test_utils.util.mock import AttributeObject
from cms.tests.test_toolbar import ToolbarTestBase
from cms.toolbar.utils import (
get_object_edit_url,
get_object_structure_url,
get_toolbar_from_request,
)
from cms.utils.placeholder import (
_get_nodelist,
_scan_placeholders,
get_placeholder_conf,
get_placeholders,
)
from cms.utils.urlutils import admin_reverse
def _get_placeholder_slots(template):
return [pl.slot for pl in get_placeholders(template)]
def _render_placeholder(placeholder, context, **kwargs):
request = context["request"]
toolbar = get_toolbar_from_request(request)
content_renderer = toolbar.content_renderer
return content_renderer.render_placeholder(placeholder, context, **kwargs)
class PlaceholderTestCase(TransactionCMSTestCase):
def setUp(self):
u = self._create_user("test", True, True)
self._login_context = self.login_user_context(u)
self._login_context.__enter__()
def tearDown(self):
self._login_context.__exit__(None, None, None)
def test_placeholder_scanning_extend(self):
placeholders = _get_placeholder_slots("placeholder_tests/test_one.html")
self.assertEqual(sorted(placeholders), sorted(["new_one", "two", "three"]))
def test_placeholder_scanning_variable_extend(self):
placeholders = _get_placeholder_slots("placeholder_tests/test_variable_extends.html")
self.assertEqual(placeholders, ["one", "two", "three", "four"])
def test_placeholder_scanning_inherit_from_variable_extend(self):
placeholders = _get_placeholder_slots("placeholder_tests/test_inherit_from_variable_extends.html")
self.assertEqual(placeholders, ["one", "two", "three", "four"])
def test_placeholder_scanning_sekizai_extend(self):
placeholders = _get_placeholder_slots("placeholder_tests/test_one_sekizai.html")
self.assertEqual(sorted(placeholders), sorted(["new_one", "two", "three"]))
def test_placeholder_scanning_include(self):
placeholders = _get_placeholder_slots("placeholder_tests/test_two.html")
self.assertEqual(sorted(placeholders), sorted(["child", "three"]))
def test_placeholder_scanning_double_extend(self):
placeholders = _get_placeholder_slots("placeholder_tests/test_three.html")
self.assertEqual(sorted(placeholders), sorted(["new_one", "two", "new_three"]))
def test_placeholder_scanning_sekizai_double_extend(self):
placeholders = _get_placeholder_slots("placeholder_tests/test_three_sekizai.html")
self.assertEqual(sorted(placeholders), sorted(["new_one", "two", "new_three"]))
def test_placeholder_scanning_complex(self):
placeholders = _get_placeholder_slots("placeholder_tests/test_four.html")
self.assertEqual(sorted(placeholders), sorted(["new_one", "child", "four"]))
def test_placeholder_scanning_super(self):
placeholders = _get_placeholder_slots("placeholder_tests/test_five.html")
self.assertEqual(sorted(placeholders), sorted(["one", "extra_one", "two", "three"]))
def test_placeholder_scanning_nested(self):
placeholders = _get_placeholder_slots("placeholder_tests/test_six.html")
self.assertEqual(sorted(placeholders), sorted(["new_one", "new_two", "new_three"]))
def test_placeholder_scanning_duplicate(self):
placeholders = self.failUnlessWarns(
DuplicatePlaceholderWarning,
'Duplicate {% placeholder "one" %} in template placeholder_tests/test_seven.html.',
_get_placeholder_slots,
"placeholder_tests/test_seven.html",
)
self.assertEqual(sorted(placeholders), sorted(["one"]))
def test_placeholder_scanning_extend_outside_block(self):
placeholders = _get_placeholder_slots("placeholder_tests/outside.html")
self.assertEqual(sorted(placeholders), sorted(["new_one", "two", "base_outside"]))
def test_placeholder_scanning_sekizai_extend_outside_block(self):
placeholders = _get_placeholder_slots("placeholder_tests/outside_sekizai.html")
self.assertEqual(sorted(placeholders), sorted(["new_one", "two", "base_outside"]))
def test_placeholder_scanning_extend_outside_block_nested(self):
placeholders = _get_placeholder_slots("placeholder_tests/outside_nested.html")
self.assertEqual(sorted(placeholders), sorted(["new_one", "two", "base_outside"]))
def test_placeholder_scanning_sekizai_extend_outside_block_nested(self):
placeholders = _get_placeholder_slots("placeholder_tests/outside_nested_sekizai.html")
self.assertEqual(sorted(placeholders), sorted(["new_one", "two", "base_outside"]))
def test_placeholder_scanning_var(self):
t = Template('{%load cms_tags %}{% include name %}{% placeholder "a_placeholder" %}')
phs = sorted(node.get_declaration().slot for node in _scan_placeholders(t.nodelist))
self.assertListEqual(phs, sorted(["a_placeholder"]))
t = Template('{% include "placeholder_tests/outside_nested_sekizai.html" %}')
phs = sorted(node.get_declaration().slot for node in _scan_placeholders(t.nodelist))
self.assertListEqual(phs, sorted(["two", "new_one", "base_outside"]))
def test_placeholder_scanning_unresolvable_extends_variable(self):
"""
Test that _scan_placeholders gracefully handles extends tags with
unresolvable variables (like CMS_TEMPLATE without a default).
Issue #8334.
"""
# Template with an extends tag using a variable that won't be in the context
template_str = (
'{% extends CMS_TEMPLATE %}{% load cms_tags %}{% block content %}{% placeholder "slot" %}{% endblock %}'
)
tpl = Template(template_str)
# Should not raise TemplateSyntaxError, just return empty list
nodes = _scan_placeholders(tpl.nodelist)
slots = [node.get_declaration().slot for node in nodes]
self.assertEqual(slots, [])
def test_placeholder_scanning_nonexistent_template_extends(self):
"""
Test that _scan_placeholders gracefully handles extends tags pointing
to nonexistent templates.
Regression test for issue #8334.
"""
# Template extending a nonexistent template
t = Template(
'{% extends "nonexistent_template.html" %}{% load cms_tags %}'
'{% block content %}{% placeholder "test_slot" %}{% endblock %}'
)
# Should not raise TemplateDoesNotExist, just return empty list
nodes = _scan_placeholders(t.nodelist)
slots = [node.get_declaration().slot for node in nodes]
self.assertEqual(slots, [])
def test_placeholder_scanning_no_object(self):
"""Placeholder scanning for a template without a toolbar object raises PlaceholderNotFound"""
context = SekizaiContext()
request = self.get_request(language="en", page=None)
toolbar = get_toolbar_from_request(request)
renderer = toolbar.get_content_renderer()
with self.assertRaises(PlaceholderNotFound):
renderer.render_obj_placeholder("someslot", context, False)
def test_fieldsets_requests(self):
response = self.client.get(admin_reverse("placeholderapp_example1_add"))
self.assertEqual(response.status_code, 200)
response = self.client.get(admin_reverse("placeholderapp_twoplaceholderexample_add"))
self.assertEqual(response.status_code, 200)
def test_page_only_plugins(self):
ex = Example1(char_1="one", char_2="two", char_3="tree", char_4="four")
ex.save()
response = self.client.get(admin_reverse("placeholderapp_example1_change", args=(ex.pk,)))
self.assertEqual(response.status_code, 200)
self.assertNotContains(response, "InheritPagePlaceholderPlugin")
def test_inter_placeholder_plugin_move(self):
ex = TwoPlaceholderExample(char_1="one", char_2="two", char_3="tree", char_4="four")
ex.save()
ph1 = ex.placeholder_1
ph2 = ex.placeholder_2
ph1_pl1 = add_plugin(ph1, "TextPlugin", "en", body="ph1 plugin1").cmsplugin_ptr
ph1_pl2 = add_plugin(ph1, "TextPlugin", "en", body="ph1 plugin2").cmsplugin_ptr
ph1_pl3 = add_plugin(ph1, "TextPlugin", "en", body="ph1 plugin3").cmsplugin_ptr
ph2_pl1 = add_plugin(ph2, "TextPlugin", "en", body="ph2 plugin1").cmsplugin_ptr
ph2_pl2 = add_plugin(ph2, "TextPlugin", "en", body="ph2 plugin2").cmsplugin_ptr
ph2_pl3 = add_plugin(ph2, "TextPlugin", "en", body="ph2 plugin3").cmsplugin_ptr
endpoint = self.get_move_plugin_uri(ph1_pl2)
# Move ph2_pl3 to position 1 on placeholder 2
data = {
"plugin_id": str(ph2_pl3.pk),
"target_language": "en",
"target_position": 1,
}
response = self.client.post(endpoint, data)
self.assertEqual(response.status_code, 200)
self.assertEqual([ph2_pl3, ph2_pl1, ph2_pl2], list(ph2.cmsplugin_set.order_by("position")))
# Move ph1_pl2 to last position on placeholder 2
data = {
"placeholder_id": str(ph2.pk),
"plugin_id": str(ph1_pl2.pk),
"target_language": "en",
"target_position": ph2.get_next_plugin_position("en", insert_order="last"),
}
response = self.client.post(endpoint, data)
self.assertEqual(response.status_code, 200)
self.assertEqual([ph1_pl1, ph1_pl3], list(ph1.cmsplugin_set.order_by("position")))
self.assertEqual([ph2_pl3, ph2_pl1, ph2_pl2, ph1_pl2], list(ph2.cmsplugin_set.order_by("position")))
def test_inter_placeholder_nested_plugin_move(self):
# Specific test to move a large number of plugins from one placeholder to a placeholder with a
# small number of plugins, between similarly size placeholders and a small number of plugins to
# a placeholder with a large number of plugins (For this test large is 10 times small.)
# These cases ensure the move operation is flexible with the relation of plugins, e.g. creates enough space
#
# To ensure the test works with different languages, plugins are created for a number of language with
# interlacing pk, the move operation only happens in the first language - others should stay unchanged.
#
language_fun = ("en", "de", "it", "de-formal")
# symmetric and asymmetric plugin numbers in target placeholder, source placeholder
# (1, 10) One plugin in the target placeholder, 10 plugins to be moved
# (5, 5) Same size situation
# (10, 1) Ten plugins in the target placeholder, 1 plugin to be moved
for n1, n2 in ((1, 10), (10, 1), (5, 5)):
ex = TwoPlaceholderExample()
ex.save()
ph1 = ex.placeholder_1
ph2 = ex.placeholder_2
ph1parent = {} # parents
ph2parent = {}
ph1children = {} # Children
ph2children = {}
for lang in language_fun:
# first create parent in target placeholder
ph1parent[lang] = add_plugin(ph1, "TextPlugin", lang, body="ph1 parent").cmsplugin_ptr
ph1children[lang] = []
for i in range(n1):
# create children in target placeholder
ph1children[lang].append(
add_plugin(ph1, "TextPlugin", lang, target=ph1parent[lang], body=f"ph1 child{i}").cmsplugin_ptr
)
# now create parent in source placeholder
ph2parent[lang] = add_plugin(ph2, "TextPlugin", lang, body="ph2 parent").cmsplugin_ptr
ph2children[lang] = []
for i in range(n2):
# followed by children in source placeholder
ph2children[lang].append(
add_plugin(ph2, "TextPlugin", lang, target=ph2parent[lang], body=f"ph2 child{i}").cmsplugin_ptr
)
# Plugins have been created in all languages. Now move the first language plugins from the source
# placeholder to the target placeholder
ph2.move_plugin(
ph2parent[language_fun[0]], # Move parent (first language only) **and** its children
target_plugin=ph1parent[language_fun[0]], # as child of parent in target placeholder
target_position=2, # First child, that is
target_placeholder=ph1,
)
# The result in theory:
# Parent Target Placeholder
# Parent Source Placeholder <-- Moved plugin parent
# Children Source Placeholder <-- Moved plugin children
# Children Target Placeholder
left = (
[ph1parent[language_fun[0]], ph2parent[language_fun[0]]]
+ ph2children[language_fun[0]]
+ ph1children[language_fun[0]]
)
# The result in practice
right = list(ph1.cmsplugin_set.filter(language=language_fun[0]).order_by("position"))
self.assertEqual(left, right)
ph1.delete()
ph2.delete()
def test_get_last_plugin_position(self):
# get_last_plugin_position is supposed to either get the position of the last plugin of the placeholder
# or the position of the last grand(!) child if a given plugin, independent of language!
# This test creates a tree of the form
# 1. Plugin 1
# 2. Plugin 2
# 3. Plugin 3
# 4. Plugin 4
# 5. Final Plugin
language_fun = ("en", "de", "it", "de-formal")
ex = Example1()
ex.save()
ph = ex.placeholder
parent = None
n = 4 # This will be the position of the last plugin
plugins = {lang: [] for lang in language_fun}
parent = dict.fromkeys(language_fun)
for i in range(n):
for lang in language_fun:
parent[lang] = add_plugin(ph, "TextPlugin", lang, target=parent[lang]).cmsplugin_ptr
plugins[lang].append(parent[lang])
for lang in language_fun:
self.assertEqual(ph.get_last_plugin_position(lang), n) # should be n
for parent in plugins[lang]:
# needs to be n also if we look at the parents
# the last plugin does not have a child hence position should be none
self.assertEqual(
ph.get_last_plugin_position(lang, parent=parent),
n if parent != plugins[lang][-1] else None,
)
add_plugin(ph, "TextPlugin", lang, target=None) # Add a top level final plugin
self.assertEqual(ph.get_last_plugin_position(lang), n + 1) # plus the last plugin
for parent in plugins[lang]:
# still needs to be n also if we look at the parents
# the last plugin does not have a child hence position should be none
self.assertEqual(
ph.get_last_plugin_position(lang, parent=parent),
n if parent != plugins[lang][-1] else None,
)
def test_get_last_plugin_position_order(self):
# This test creates a tree like this
# 1. Plugin n
# 2. Plugin n-1
# 3. Plugin n-2
# ...
# n. Plugin 1
# The children are created with the 'first-child' option, i.e. in reverse order: new children
# are prepended to the existing ones. Still get_last_plugin_position needs to return the position
# n if the last plugin (though it was created first).
# To make this more fun we interlace plugins with other languages to ensure they do not influence
# the result.
language_fun = ("en", "de", "it", "en-US")
ex = Example1()
ex.save()
ph = ex.placeholder
n = 15 # This will be the position of the last plugin
for i in range(n):
# Use 'first-child' order causes pk and position run inversely
for language in language_fun:
add_plugin(ph, "TextPlugin", language, "first-child").cmsplugin_ptr
self.assertEqual(ph.get_last_plugin_position("en"), n) # should be n
def test_copy_plugin(self):
superuser = self.get_superuser()
page_en = create_page("CopyPluginTestPage (EN)", "nav_playground.html", "en")
ph_source = page_en.get_placeholders("en").get(slot="body")
plugin = add_plugin(ph_source, "LinkPlugin", "en", name="A Link", external_link="https://www.django-cms.org")
endpoint = self.get_copy_plugin_uri(plugin)
user_settings = UserSettings.objects.create(
language="en",
user=superuser,
clipboard=Placeholder.objects.create(),
)
data = {
"source_plugin_id": plugin.pk,
"source_placeholder_id": ph_source.pk,
"source_language": plugin.language,
"target_language": "en",
"target_placeholder_id": user_settings.clipboard.pk,
}
with self.login_user_context(superuser):
# Copy plugins into the clipboard
response = self.client.post(endpoint, data)
self.assertEqual(response.status_code, 200)
clipboard_plugins = user_settings.clipboard.get_plugins()
self.assertTrue(clipboard_plugins.filter(plugin_type="LinkPlugin").exists())
self.assertEqual(len(clipboard_plugins), 1)
def test_copy_plugin_without_custom_model(self):
superuser = self.get_superuser()
page_en = create_page("CopyPluginTestPage (EN)", "nav_playground.html", "en")
ph_source = page_en.get_placeholders("en").get(slot="body")
plugin = add_plugin(ph_source, "NoCustomModel", "en")
endpoint = self.get_copy_plugin_uri(plugin)
user_settings = UserSettings.objects.create(
language="en",
user=superuser,
clipboard=Placeholder.objects.create(),
)
data = {
"source_plugin_id": plugin.pk,
"source_placeholder_id": ph_source.pk,
"source_language": plugin.language,
"target_language": "en",
"target_placeholder_id": user_settings.clipboard.pk,
}
with self.login_user_context(superuser):
# Copy plugins into the clipboard
response = self.client.post(endpoint, data)
self.assertEqual(response.status_code, 200)
clipboard_plugins = user_settings.clipboard.get_plugins()
self.assertTrue(clipboard_plugins.filter(plugin_type="NoCustomModel").exists())
self.assertEqual(len(clipboard_plugins), 1)
def test_paste_plugin_from_clipboard(self):
superuser = self.get_superuser()
page_en = create_page("CopyPluginTestPage (EN)", "nav_playground.html", "en")
ph_target = page_en.get_placeholders("en").get(slot="body")
add_plugin(ph_target, "LinkPlugin", "en", name="A Link", external_link="https://www.django-cms.org")
user_settings = UserSettings.objects.create(
language="en",
user=superuser,
clipboard=Placeholder.objects.create(),
)
plugin = add_plugin(
user_settings.clipboard,
"LinkPlugin",
language="de", # Test x-language
name="A Link",
external_link="https://www.django-cms.org",
)
endpoint = self.get_move_plugin_uri(plugin)
# Paste LinkPlugin to first position
data = {
"plugin_id": plugin.pk,
"move_a_copy": True,
"target_language": "en",
"target_position": 1,
"placeholder_id": ph_target.pk,
}
with self.login_user_context(superuser):
# Paste plugin from the clipboard into target placeholder
response = self.client.post(endpoint, data)
self.assertEqual(response.status_code, 200)
plugins = ph_target.get_plugins("en")
self.assertTrue(plugins.filter(plugin_type="LinkPlugin").exists())
self.assertEqual(len(plugins), 2)
# Paste LinkPlugin as child of first LinkPlugin
target = ph_target.get_plugins("en").get(position=1)
data = {
"plugin_id": plugin.pk,
"move_a_copy": True,
"target_language": "en",
"target_position": 2,
"plugin_parent": target.pk,
"placeholder_id": ph_target.pk,
}
with self.login_user_context(superuser):
# Paste plugin from the clipboard into target placeholder
response = self.client.post(endpoint, data)
self.assertEqual(response.status_code, 200)
plugins = ph_target.get_plugins("en")
self.assertTrue(plugins.filter(plugin_type="LinkPlugin").exists())
self.assertEqual(len(plugins), 3)
def test_placeholder_render_ghost_plugin(self):
"""
Tests a placeholder won't render a ghost plugin.
"""
page_en = create_page("page_en", "col_two.html", "en")
placeholder_en = page_en.get_placeholders("en").get(slot="col_left")
CMSPlugin.objects.create(
language="en",
plugin_type="LinkPlugin",
position=1,
placeholder=placeholder_en,
parent=None,
)
add_plugin(
placeholder_en,
"LinkPlugin",
"en",
name="name",
external_link="http://example.com/",
)
context_en = SekizaiContext()
context_en["request"] = self.get_request(language="en", page=page_en)
content_en = _render_placeholder(placeholder_en, context_en)
self.assertEqual(content_en.strip(), '<a href="http://example.com/">name</a>')
def test_placeholder_render_ghost_plugin_with_child(self):
"""
Tests a placeholder won't render a ghost plugin or any of it's children.
"""
page_en = create_page("page_en", "col_two.html", "en")
placeholder_en = page_en.get_placeholders("en").get(slot="col_left")
plugin = CMSPlugin.objects.create(
language="en",
plugin_type="LinkPlugin",
position=1,
placeholder=placeholder_en,
parent=None,
)
add_plugin(
placeholder_en,
"LinkPlugin",
"en",
target=plugin,
name="invalid",
external_link="http://example.com/",
)
add_plugin(
placeholder_en,
"LinkPlugin",
"en",
name="valid",
external_link="http://example.com/",
)
context_en = SekizaiContext()
context_en["request"] = self.get_request(language="en", page=page_en)
content_en = _render_placeholder(placeholder_en, context_en)
self.assertEqual(content_en.strip(), '<a href="http://example.com/">valid</a>')
@override_settings(CMS_PERMISSION=False)
def test_databridge_contains_changes(self):
"""
Checks #1366 error condition.
When adding/editing a plugin whose icon_src() method returns a URL
containing a hyphen, the hyphen is escaped by django escapejs resulting
in a incorrect URL
"""
ex = Example1(char_1="one", char_2="two", char_3="tree", char_4="four")
ex.save()
ph1 = ex.placeholder
###
# add the test plugin
###
test_plugin = add_plugin(ph1, "EmptyPlugin", "en")
test_plugin.save()
endpoint = self.get_change_plugin_uri(test_plugin)
response = self.client.post(endpoint, {})
self.assertContains(response, '<script id="data-bridge" type="application/json">')
self.assertContains(response, f'title=\\"EmptyPlugin ID: {test_plugin.pk}\\"')
@override_settings(CMS_PERMISSION=False)
def test_nested_plugin_escapejs_page(self):
"""
Sibling test of the above, on a page.
#1366 does not apply to placeholder defined in a page
"""
page = create_page("page", "col_two.html", "en")
ph1 = page.get_placeholders("en").get(slot="col_left")
###
# add the test plugin
###
test_plugin = add_plugin(ph1, "EmptyPlugin", "en")
test_plugin.save()
endpoint = self.get_change_plugin_uri(test_plugin)
response = self.client.post(endpoint, {})
self.assertContains(response, '<script id="data-bridge" type="application/json">')
self.assertContains(response, f'title=\\"EmptyPlugin ID: {test_plugin.pk}\\"')
@override_settings(CMS_PERMISSION=False)
def test_change_message_escapes_plugin_representation(self):
"""
The plugin change message ends up in the data bridge as raw JSON, where
no template autoescaping applies. A plugin's string representation is
editor-controlled, so it has to be escaped before it gets there.
"""
payload = '<img src=x onerror="alert(1)">'
page = create_page("page", "col_two.html", "en")
ph1 = page.get_placeholders("en").get(slot="col_left")
test_plugin = add_plugin(
ph1, "LinkPlugin", "en", name="A Link", external_link="https://www.django-cms.org"
)
endpoint = self.get_change_plugin_uri(test_plugin)
response = self.client.post(
endpoint, {"name": payload, "external_link": "https://www.django-cms.org"}
)
bridge = json.loads(
re.search(
r'<script id="data-bridge" type="application/json">(.*?)</script>',
response.content.decode(),
re.S,
).group(1)
)
message = bridge["messages"][0]["message"]
self.assertNotIn(payload, message)
self.assertIn("&lt;img src=x onerror=&quot;alert(1)&quot;&gt;", message)
def test_placeholder_scanning_fail(self):
self.assertRaises(TemplateSyntaxError, _get_placeholder_slots, "placeholder_tests/test_eleven.html")
def test_placeholder_tag(self):
request = self.get_request("/", language=settings.LANGUAGES[0][0])
template = "{% load cms_tags %}{% render_placeholder placeholder %}"
output = self.render_template_obj(template, {}, request)
self.assertEqual(output, "")
placeholder = Placeholder.objects.create(slot="test")
output = self.render_template_obj(template, {"placeholder": placeholder}, request)
self.assertEqual(output, "")
self.assertEqual(placeholder.get_plugins().count(), 0)
add_plugin(placeholder, "TextPlugin", settings.LANGUAGES[0][0], body="test")
self.assertEqual(placeholder.get_plugins().count(), 1)
placeholder = self.reload(placeholder)
output = self.render_template_obj(template, {"placeholder": placeholder}, request)
self.assertEqual(output, "test")
def test_placeholder_tag_language(self):
template = "{% load cms_tags %}{% render_placeholder placeholder language language %}"
placeholder = Placeholder.objects.create(slot="test")
add_plugin(placeholder, "TextPlugin", "en", body="English")
add_plugin(placeholder, "TextPlugin", "de", body="Deutsch")
request = self.get_request("/")
output = self.render_template_obj(template, {"placeholder": placeholder, "language": "en"}, request)
self.assertEqual(output.strip(), "English")
del placeholder._plugins_cache
output = self.render_template_obj(template, {"placeholder": placeholder, "language": "de"}, request)
self.assertEqual(output.strip(), "Deutsch")
def test_get_placeholder_conf(self):
TEST_CONF = {
"main": {
"name": "main content",
"plugins": ["TextPlugin", "LinkPlugin"],
"default_plugins": [
{
"plugin_type": "TextPlugin",
"values": {"body": "<p>Some default text</p>"},
},
],
},
"layout/home.html main": {
"name": "main content with FilerImagePlugin and limit",
"plugins": ["TextPlugin", "FilerImagePlugin", "LinkPlugin"],
"inherit": "main",
"limits": {"global": 1},
},
"layout/other.html main": {
"name": "main content with FilerImagePlugin and no limit",
"inherit": "layout/home.html main",
"limits": {},
"excluded_plugins": ["LinkPlugin"],
},
None: {
"name": "All",
"plugins": ["FilerImagePlugin", "LinkPlugin"],
"limits": {},
},
}
with override_placeholder_conf(CMS_PLACEHOLDER_CONF=TEST_CONF):
# test no inheritance
returned = get_placeholder_conf("plugins", "main")
self.assertEqual(returned, TEST_CONF["main"]["plugins"])
# test no inherited value with inheritance enabled
returned = get_placeholder_conf("plugins", "main", "layout/home.html")
self.assertEqual(returned, TEST_CONF["layout/home.html main"]["plugins"])
# test direct inherited value
returned = get_placeholder_conf("plugins", "main", "layout/other.html")
self.assertEqual(returned, TEST_CONF["layout/home.html main"]["plugins"])
# test excluded_plugins key
returned = get_placeholder_conf("excluded_plugins", "main", "layout/other.html")
self.assertEqual(returned, TEST_CONF["layout/other.html main"]["excluded_plugins"])
# test grandparent inherited value
returned = get_placeholder_conf("default_plugins", "main", "layout/other.html")
self.assertEqual(returned, TEST_CONF["main"]["default_plugins"])
# test generic configuration
returned = get_placeholder_conf("plugins", "something")
self.assertEqual(returned, TEST_CONF[None]["plugins"])
# test doubly-inherited
returned = get_placeholder_conf("default_plugins", "layout/other.html main")
self.assertEqual(returned, TEST_CONF["main"]["default_plugins"])
def test_get_placeholder_conf_chained_inheritance(self):
"""
A two-level ``inherit`` chain resolves transitively without leaking
unrelated top-level conf keys into the slot config and without
mutating the inherited-from slot's own configuration.
Regression test for resolve_inheritance merging the whole top-level
CMS_PLACEHOLDER_CONF dict instead of the inherited slot's config, and
for the resolved base config being updated in place.
"""
TEST_CONF = {
"base": {"plugins": ["TextPlugin"], "name": "Base"},
"middle": {"inherit": "base", "limits": {"global": 5}},
"leaf": {"inherit": "middle", "name": "Leaf"},
}
pristine = {
"base": {"plugins": ["TextPlugin"], "name": "Base"},
"middle": {"inherit": "base", "limits": {"global": 5}},
"leaf": {"inherit": "middle", "name": "Leaf"},
}
with override_placeholder_conf(CMS_PLACEHOLDER_CONF=TEST_CONF):
# leaf inherits base's plugins through middle
self.assertEqual(get_placeholder_conf("plugins", "leaf"), ["TextPlugin"])
# leaf inherits middle's own limits
self.assertEqual(get_placeholder_conf("limits", "leaf"), {"global": 5})
# leaf keeps its own name
self.assertEqual(get_placeholder_conf("name", "leaf"), "Leaf")
# middle resolves to base's plugins plus its own limits
self.assertEqual(get_placeholder_conf("plugins", "middle"), ["TextPlugin"])
self.assertEqual(get_placeholder_conf("name", "middle"), "Base")
# base itself is untouched by the inheritance resolution
self.assertEqual(get_placeholder_conf("name", "base"), "Base")
self.assertIsNone(get_placeholder_conf("limits", "base"))
# resolution must not write into the user's settings dict
self.assertEqual(TEST_CONF, pristine)
def test_placeholder_name_conf(self):
page_en = create_page("page_en", "col_two.html", "en")
placeholder_1 = page_en.get_placeholders("en").get(slot="col_left")
placeholder_2 = Placeholder.objects.create(slot="col_left")
placeholder_3 = Placeholder.objects.create(slot="no_name")
TEST_CONF = {
"col_left": {
"name": "renamed left column",
},
"col_two.html col_left": {
"name": "left column",
},
None: {
"name": "fallback",
},
}
with override_placeholder_conf(CMS_PLACEHOLDER_CONF=TEST_CONF):
self.assertEqual(force_str(placeholder_1.get_label()), "left column")
self.assertEqual(force_str(placeholder_2.get_label()), "renamed left column")
self.assertEqual(force_str(placeholder_3.get_label()), "fallback")
del TEST_CONF[None]
with override_placeholder_conf(CMS_PLACEHOLDER_CONF=TEST_CONF):
self.assertEqual(force_str(placeholder_1.get_label()), "left column")
self.assertEqual(force_str(placeholder_2.get_label()), "renamed left column")
self.assertEqual(force_str(placeholder_3.get_label()), "No_Name")
def test_placeholders_from_blocks_order(self):
placeholders = _get_placeholder_slots("placeholder_tests/test_with_block.html")
self.assertEqual(placeholders, ["one", "two", "three", "four"])
def test_placeholder_scanning_nested_super(self):
placeholders = _get_placeholder_slots("placeholder_tests/nested_super_level1.html")
self.assertEqual(sorted(placeholders), sorted(["level1", "level2", "level3", "level4"]))
def test_placeholder_field_db_table(self):
"""
Test for leaking Model._meta.db_table monkeypatching on SQLite (#3891).
"""
example = Category.objects.create(
name="category",
parent=None,
depth=1,
)
self.assertEqual(example.description._get_attached_model(), Category)
def test_placeholder_field_dynamic_slot_generation(self):
instance = DynamicPlaceholderSlotExample.objects.create(char_1="slot1", char_2="slot2")
self.assertEqual(instance.char_1, instance.placeholder_1.slot)
self.assertEqual(instance.char_2, instance.placeholder_2.slot)
def test_placeholder_field_dynamic_slot_update(self):
instance = DynamicPlaceholderSlotExample.objects.create(char_1="slot1", char_2="slot2")
# Plugin counts
old_placeholder_1_plugin_count = len(instance.placeholder_1.get_plugins())
old_placeholder_2_plugin_count = len(instance.placeholder_2.get_plugins())
# Switch around the slot names
instance.char_1, instance.char_2 = instance.char_2, instance.char_1
# Store the ids before save, to test that a new placeholder is NOT created.
placeholder_1_id = instance.placeholder_1.pk
placeholder_2_id = instance.placeholder_2.pk
# Save instance
instance.save()
current_placeholder_1_plugin_count = len(instance.placeholder_1.get_plugins())
current_placeholder_2_plugin_count = len(instance.placeholder_2.get_plugins())
# Now test that the placeholder slots have changed
self.assertEqual(instance.char_2, "slot1")
self.assertEqual(instance.char_1, "slot2")
# Test that a new placeholder was never created
self.assertEqual(instance.placeholder_1.pk, placeholder_1_id)
self.assertEqual(instance.placeholder_2.pk, placeholder_2_id)
# And test the plugin counts remain the same
self.assertEqual(old_placeholder_1_plugin_count, current_placeholder_1_plugin_count)
self.assertEqual(old_placeholder_2_plugin_count, current_placeholder_2_plugin_count)
def test_plugins_prepopulate(self):
"""Tests prepopulate placeholder configuration"""
conf = {
"col_left": {
"default_plugins": [
{
"plugin_type": "TextPlugin",
"values": {"body": "<p>en default body 1</p>"},
},
{
"plugin_type": "TextPlugin",
"values": {"body": "<p>en default body 2</p>"},
},
]
},
}
with override_placeholder_conf(CMS_PLACEHOLDER_CONF=conf):
page = create_page("page_en", "col_two.html", "en")
placeholder = page.get_placeholders("en").get(slot="col_left")
context = SekizaiContext()
context["request"] = self.get_request(language="en", page=page)
# Our page should have "en default body 1" AND "en default body 2"
content = _render_placeholder(placeholder, context)
self.assertRegex(content, r"^<p>en default body 1</p>\s*<p>en default body 2</p>$")
def test_circular_inheritance_in_placeholder_conf_raises(self):
circular_conf = {
"slot_a": {"inherit": "slot_b"},
"slot_b": {"inherit": "slot_c"},
"slot_c": {"inherit": "slot_a"},
}
with self.assertRaises(ImproperlyConfigured):
with override_placeholder_conf(CMS_PLACEHOLDER_CONF=circular_conf):
# Attempt to access any slot to trigger the config resolution
page = create_page("circular_page", "col_two.html", "en")
placeholder = page.get_placeholders("en").get(slot="slot_a")
# Accessing the label should trigger the circular inheritance detection
force_str(placeholder.get_label())
def test_plugins_children_prepopulate(self):
"""
Validate a default textplugin with a nested default link plugin
"""
conf = {
"col_left": {
"default_plugins": [
{
"plugin_type": "TextPlugin",
"values": {"body": "<p>body %(_tag_child_1)s and %(_tag_child_2)s</p>"},
"children": [
{
"plugin_type": "LinkPlugin",
"values": {"name": "django", "external_link": "https://www.djangoproject.com/"},
},
{
"plugin_type": "LinkPlugin",
"values": {"name": "django-cms", "external_link": "https://www.django-cms.org"},
},
],
},
]
},
}
with override_placeholder_conf(CMS_PLACEHOLDER_CONF=conf):
page = create_page("page_en", "col_two.html", "en")
placeholder = page.get_placeholders("en").get(slot="col_left")
context = SekizaiContext()
context["request"] = self.get_request(language="en", page=page)
_render_placeholder(placeholder, context)
plugins = placeholder.get_plugins_list()
self.assertEqual(len(plugins), 3)
self.assertEqual(plugins[0].plugin_type, "TextPlugin")
self.assertEqual(plugins[1].plugin_type, "LinkPlugin")
self.assertEqual(plugins[2].plugin_type, "LinkPlugin")
self.assertTrue(plugins[1].parent == plugins[2].parent and plugins[1].parent == plugins[0])
def test_placeholder_pk_thousands_format(self):
page = create_page("page", "nav_playground.html", "en")
title = page.get_content_obj("en")
for placeholder in page.get_placeholders("en"):
title.placeholders.remove(placeholder)
placeholder.pk += 1000
placeholder.save()
title.placeholders.add(placeholder)
page.reload()
for placeholder in page.get_placeholders("en"):
add_plugin(placeholder, "TextPlugin", "en", body="body")
with self.settings(USE_THOUSAND_SEPARATOR=True, USE_L10N=True):
# Superuser
user = self.get_superuser()
self.client.login(
username=getattr(user, get_user_model().USERNAME_FIELD),
password=getattr(user, get_user_model().USERNAME_FIELD),
)
endpoint = get_object_edit_url(title)
response = self.client.get(endpoint)
for placeholder in page.get_placeholders("en"):
self.assertContains(response, '"placeholder_id": "%s"' % placeholder.pk)
self.assertNotContains(
response, '"placeholder_id": "%s"' % format(placeholder.pk, ".", grouping=3, thousand_sep=",")
)
self.assertNotContains(
response, '"plugin_id": "%s"' % format(placeholder.pk, ".", grouping=3, thousand_sep=",")
)
self.assertNotContains(
response,
'"clipboard": "%s"'
% format(response.context["request"].toolbar.clipboard.pk, ".", grouping=3, thousand_sep=","),
)
def test_placeholder_languages_model(self):
"""
Checks the retrieval of filled languages for a placeholder in a django
model
"""
avail_langs = {"en", "de", "fr"}
# Setup instance
ex = Example1(char_1="one", char_2="two", char_3="tree", char_4="four")
ex.save()
###
# add the test plugin
###
for lang in avail_langs:
add_plugin(ex.placeholder, "EmptyPlugin", lang)
# reload instance from database
ex = Example1.objects.get(pk=ex.pk)
# get languages
langs = [lang["code"] for lang in ex.placeholder.get_filled_languages(Site.objects.get_current().pk)]
self.assertEqual(avail_langs, set(langs))
def test_placeholder_languages_page(self):
"""
Checks the retrieval of filled languages for a placeholder in a django
model
"""
avail_langs = {"en", "de", "fr"}
# Setup instances
page = create_page("test page", "col_two.html", "en")
for lang in avail_langs:
if lang != "en":
create_page_content(lang, "test page %s" % lang, page)
placeholder = page.get_placeholders("en").get(slot="col_sidebar")
###
# add the test plugin
###
for lang in avail_langs:
add_plugin(placeholder, "EmptyPlugin", lang)
# reload placeholder from database
placeholder = page.get_placeholders("en").get(slot="col_sidebar")
# get languages
langs = [lang["code"] for lang in placeholder.get_filled_languages(Site.objects.get_current().pk)]
self.assertEqual(avail_langs, set(langs))
@override_settings(
TEMPLATE_LOADERS=(
(
"django.template.loaders.cached.Loader",
(
"django.template.loaders.filesystem.Loader",
"django.template.loaders.app_directories.Loader",
),
),
)
)
def test_cached_template_not_corrupted_by_placeholder_scan(self):
"""
This is the test for the low-level code that caused the bug:
the placeholder scan corrupts the nodelist of the extends node,
which is retained by the cached template loader, and future
renders of that template will render the super block twice.
"""
nodelist = _get_nodelist(get_template("placeholder_tests/test_super_extends_2.html"))
self.assertNotIn(
"one",
nodelist[0].blocks.keys(),
"test_super_extends_1.html contains a block called 'one', " "but _2.html does not.",
)
_get_placeholder_slots("placeholder_tests/test_super_extends_2.html")
nodelist = _get_nodelist(get_template("placeholder_tests/test_super_extends_2.html"))
self.assertNotIn(
"one",
nodelist[0].blocks.keys(),
"test_super_extends_1.html still should not contain a block "
"called 'one' after rescanning placeholders.",
)
@override_settings(
TEMPLATE_LOADERS=(
(
"django.template.loaders.cached.Loader",
(
"django.template.loaders.filesystem.Loader",
"django.template.loaders.app_directories.Loader",
),
),
)
)
def test_super_extends_not_corrupted_by_placeholder_scan(self):
"""
This is the test for the symptom of the bug: because the block
context now contains two copies of the inherited block, that block
will be executed twice, and if it adds content to {{block.super}},
that content will be added twice.
"""
template = get_template("placeholder_tests/test_super_extends_2.html")
output = template.render({})
self.assertEqual(["Whee"], [o for o in output.split("\n") if "Whee" in o])
_get_placeholder_slots("placeholder_tests/test_super_extends_2.html")
template = get_template("placeholder_tests/test_super_extends_2.html")
output = template.render({})
self.assertEqual(["Whee"], [o for o in output.split("\n") if "Whee" in o])
def test_sets_source_when_title_is_created(self):
"""
This tests when a title is created, the source field for the
created placeholders are set to that title.
"""
page = create_page("test page en", "col_two.html", "en")
# check for en
page_content_en = self.get_pagecontent_obj(page)
self.assertQuerySetEqual(
Placeholder.objects.get_for_obj(page_content_en),
page_content_en.get_placeholders(),
transform=lambda x: x,
ordered=False,
)
# check for another language = de
page_content_de = create_page_content("de", "test page de", page)
self.assertQuerySetEqual(
Placeholder.objects.get_for_obj(page_content_de),
page_content_de.get_placeholders(),
transform=lambda x: x,
ordered=False,
)
def test_sets_source_when_title_is_copied(self):
"""
This tests when a title is copied, the source field for the
created placeholders are set to the new title.
"""
page = create_page("source page", "col_two.html", "en")
with self.login_user_context(self.get_superuser()):
new_page = self.copy_page(page, page, position=1)
page_content = self.get_pagecontent_obj(page)
new_page_content = self.get_pagecontent_obj(new_page)
page_content_plhs = Placeholder.objects.get_for_obj(page_content)
new_page_content_plhs = Placeholder.objects.get_for_obj(new_page_content)
self.assertEqual(page_content_plhs.count(), new_page_content_plhs.count())
def test_sets_source_when_external_object_is_rendered(self):
"""
This tests the implementation for external objects to use the {% placeholder %}
template tag where placeholders are created with the source field
set to the current object.
"""
from cms.utils.placeholder import get_declared_placeholders_for_obj
poll = FancyPoll.objects.create(name="poll 1")
# Go to the poll for the first time
response = self.client.get(poll.get_absolute_url())
placeholders = Placeholder.objects.get_for_obj(poll)
self.assertEqual(placeholders.count(), 2)
# Now go to edit mode
with self.login_user_context(self.get_superuser()):
response = self.client.get(get_object_edit_url(poll))
placeholders = Placeholder.objects.get_for_obj(poll)
self.assertEqual(placeholders.count(), 2)
# Now go to structure mode
with self.login_user_context(self.get_superuser()):
response = self.client.get(get_object_structure_url(poll))
placeholders = Placeholder.objects.get_for_obj(poll)
self.assertEqual(placeholders.count(), 2)
declared_placeholders = get_declared_placeholders_for_obj(poll)
self.assertEqual(len(declared_placeholders), 2)
for placeholder in declared_placeholders:
self.assertContains(
response,
'<template data-cms id="cms-plugin-child-classes-%s" type="text/cms-template">'
% placeholders.get(slot=placeholder.slot).pk,
)
# Change the template
poll.template = "fancy_poll_app/detail2.html"
poll.save()
# Now go to structure mode again
with self.login_user_context(self.get_superuser()):
response = self.client.get(get_object_structure_url(poll))
placeholders = Placeholder.objects.get_for_obj(poll)
# By this time rescanning should have occurred
# and created new placeholders
placeholders = Placeholder.objects.get_for_obj(poll)
self.assertEqual(placeholders.count(), 5)
declared_placeholders = get_declared_placeholders_for_obj(poll)
self.assertEqual(len(declared_placeholders), 3)
for placeholder in declared_placeholders:
self.assertContains(
response,
'<template data-cms id="cms-plugin-child-classes-%s" type="text/cms-template">'
% placeholders.get(slot=placeholder.slot).pk,
)
def test_placeholder_relation_field(self):
"""
This tests the PlaceholderRelationField where it returns (does a reverse relation)
the placeholders for the attached object.
"""
poll = FancyPoll.objects.create(name="poll 1")
with self.login_user_context(self.get_superuser()):
self.client.get(get_object_edit_url(poll))
self.assertQuerySetEqual(
poll.placeholders.all(), Placeholder.objects.get_for_obj(poll), transform=lambda x: x, ordered=False
)
def test_placeholder_relation_field_get_slot(self):
"""
This tests the get slot method of PlaceholderRelationField
"""
from cms.utils.placeholder import get_placeholder_from_slot
poll = FancyPoll.objects.create(name="poll 1")
slot_1 = get_placeholder_from_slot(poll.placeholders, slot="slot_1") # Get placeholder
self.assertTrue(isinstance(slot_1, Placeholder)) # Correct type
self.assertEqual(slot_1.slot, "slot_1") # Correct slot
self.assertEqual(poll.placeholders.all().count(), 1) # Has been created?
slot_2 = get_placeholder_from_slot(poll.placeholders, "slot_2") # Get 2nd placeholder
self.assertEqual(slot_2.slot, "slot_2") # right slot
self.assertEqual(poll.placeholders.all().count(), 2) # Two should have been created
self.assertEqual(slot_1, get_placeholder_from_slot(poll.placeholders, "slot_1")) # Still the first slot
@override_settings(CMS_PERMISSION=False)
class PlaceholderModelTests(ToolbarTestBase, CMSTestCase):
def get_mock_user(self, superuser):
return AttributeObject(
is_superuser=superuser,
has_perm=lambda string: False,
)
def get_mock_request(self, superuser=True):
return AttributeObject(superuser=superuser, user=self.get_mock_user(superuser))
def test_check_placeholder_permissions_ok_for_superuser(self):
ph = Placeholder.objects.create(slot="test", default_width=300)
user = self.get_mock_user(True)
result = ph.has_change_permission(user)
self.assertTrue(result)
def test_check_placeholder_permissions_nok_for_user(self):
ph = Placeholder.objects.create(slot="test", default_width=300)
user = self.get_mock_user(False)
result = ph.has_change_permission(user)
self.assertFalse(result)
def test_check_placeholder_permissions_with_object_perms(self):
from cms.test_utils.project.objectpermissionsapp.models import UserObjectPermission
ph = Placeholder.objects.create(slot="test", default_width=300)
user = self._create_user("test")
ex = Example1.objects.create(char_1="one", char_2="two", char_3="three", char_4="four")
ph.source = ex
# Initially user has no permissions
result = ph.has_change_permission(user)
self.assertFalse(result)
# Grant object-level permission
UserObjectPermission.objects.assign_perm("change_example1", user, ex)
result = ph.has_change_permission(user)
self.assertTrue(result)
def test_check_unicode_rendering(self):
ph = Placeholder.objects.create(slot="test", default_width=300)
result = force_str(ph)
self.assertEqual(result, "test")
def test_excercise_get_attached_model(self):
ph = Placeholder.objects.create(slot="test", default_width=300)
result = ph._get_attached_model()
self.assertEqual(result, None) # Simple PH - no model
def test_excercise_get_attached_field(self):
ph = Placeholder.objects.create(slot="test", default_width=300)
self.assertEqual(ph.source, None) # Simple PH - no source
def test_excercise_get_attached_models_notplugins(self):
ex = Example1(char_1="one", char_2="two", char_3="tree", char_4="four")
ex.save()
ph = ex.placeholder
result = ph._get_attached_model()
self.assertEqual(result, Example1) # Simple PH - Example1 model
add_plugin(ph, "TextPlugin", "en", body="en body")
result = ph._get_attached_model()
self.assertEqual(result, Example1) # Simple PH still one Example1 model
def test_excercise_get_placeholder_source(self):
ex = Example1(
char_1="one",
char_2="two",
char_3="tree",
char_4="four",
)
ex.save()
ph = ex.placeholder
self.assertEqual(ph.source, ex) # Simple PH - with source reference
def test_repr(self):
unsaved_ph = Placeholder()
self.assertIn("id=None", repr(unsaved_ph))
self.assertIn("slot=''", repr(unsaved_ph))
saved_ph = Placeholder.objects.create(slot="test")
self.assertIn(f"id={saved_ph.pk}", repr(saved_ph))
self.assertIn(f"slot='{saved_ph.slot}'", repr(saved_ph))
class PlaceholderConfTests(TestCase):
def test_get_all_plugins_single_page(self):
page = create_page("page", "col_two.html", "en")
placeholder = page.get_placeholders("en").get(slot="col_left")
conf = {
"col_two": {
"plugins": ["TextPlugin", "LinkPlugin"],
},
"col_two.html col_left": {
"plugins": ["LinkPlugin"],
},
}
LinkPlugin = plugin_pool.get_plugin("LinkPlugin")
with override_placeholder_conf(CMS_PLACEHOLDER_CONF=conf):
plugins = list(plugin_pool.get_all_plugins(placeholder.slot, placeholder.source))
self.assertEqual(len(plugins), 1, plugins)
self.assertEqual(plugins[0], LinkPlugin)
def test_get_all_plugins_inherit(self):
parent = create_page("parent", "col_two.html", "en")
page = create_page("page", constants.TEMPLATE_INHERITANCE_MAGIC, "en", parent=parent)
placeholder = page.get_placeholders("en").get(slot="col_left")
conf = {
"col_two": {
"plugins": ["TextPlugin", "LinkPlugin"],
},
"col_two.html col_left": {
"plugins": ["LinkPlugin"],
},
}
LinkPlugin = plugin_pool.get_plugin("LinkPlugin")
with override_placeholder_conf(CMS_PLACEHOLDER_CONF=conf):
plugins = list(plugin_pool.get_all_plugins(placeholder.slot, placeholder.source, root_plugin=True))
self.assertEqual(len(plugins), 1, plugins)
self.assertEqual(plugins[0], LinkPlugin)
def test_is_allowed_in_slot(self):
LinkPlugin = plugin_pool.get_plugin("LinkPlugin")
# No restriction (default): allowed everywhere, including unbound (None) queries.
self.assertTrue(LinkPlugin.is_allowed_in_slot("content"))
self.assertTrue(LinkPlugin.is_allowed_in_slot(None))
with patch.object(LinkPlugin, "allowed_slots", ["content", "footer_*"]):
# Exact match and glob match are allowed; an unlisted slot is not.
self.assertTrue(LinkPlugin.is_allowed_in_slot("content"))
self.assertTrue(LinkPlugin.is_allowed_in_slot("footer_left"))
self.assertFalse(LinkPlugin.is_allowed_in_slot("sidebar"))
# An unbound query is always allowed, even with a restriction set.
self.assertTrue(LinkPlugin.is_allowed_in_slot(None))
with patch.object(LinkPlugin, "allowed_slots", []):
# An empty list forbids every slot.
self.assertFalse(LinkPlugin.is_allowed_in_slot("content"))
with patch.object(LinkPlugin, "allowed_slots", "content"):
# A bare string is treated as a single pattern, not iterated per character.
self.assertTrue(LinkPlugin.is_allowed_in_slot("content"))
self.assertFalse(LinkPlugin.is_allowed_in_slot("sidebar"))
self.assertFalse(LinkPlugin.is_allowed_in_slot("c"))
def test_get_all_plugins_respects_allowed_slots(self):
page = create_page("page", "col_two.html", "en")
placeholder = page.get_placeholders("en").get(slot="col_left")
LinkPlugin = plugin_pool.get_plugin("LinkPlugin")
with patch.object(LinkPlugin, "allowed_slots", ["col_right"]):
available = list(plugin_pool.get_all_plugins(placeholder.slot, placeholder.source))
self.assertNotIn(LinkPlugin, available)
# The plugin is available in a slot it allows.
allowed = list(plugin_pool.get_all_plugins("col_right", placeholder.source))
self.assertIn(LinkPlugin, allowed)
def test_get_plugin_disallowed_in_slot(self):
from cms.utils.plugins import get_plugin_disallowed_in_slot
LinkPlugin = plugin_pool.get_plugin("LinkPlugin")
with patch.object(LinkPlugin, "allowed_slots", ["content"]):
# The offending plugin type is returned for a disallowed slot.
self.assertEqual(
get_plugin_disallowed_in_slot(["LinkPlugin"], "sidebar"), "LinkPlugin"
)
# Every plugin is allowed -> nothing is reported.
self.assertIsNone(get_plugin_disallowed_in_slot(["LinkPlugin"], "content"))
# Unregistered plugin types are skipped (treated as allowed).
self.assertIsNone(
get_plugin_disallowed_in_slot(["DoesNotExistPlugin"], "sidebar")
)
class PlaceholderPluginTestsBase(CMSTestCase):
def _create_placeholder(self, slot="main"):
return Placeholder.objects.create(slot=slot)
def _create_plugin(self, placeholder, position, parent=None):
base = CMSPlugin.objects.create(
language="en",
plugin_type="StylePlugin",
parent=parent,
position=position,
placeholder=placeholder,
)
plugin_model = base.plugin_class.model
plugin = plugin_model()
base.set_base_attr(plugin)
plugin.save()
return plugin
def _unpack_descendants(self, parent):
for child in parent.cmsplugin_set.all():
yield child.pk
yield from self._unpack_descendants(child)
def setUp(self):
self.placeholder = self._create_placeholder()
self.create_plugins(self.placeholder)
def create_plugins(self, placeholder):
for i in range(1, 9):
parent = add_plugin(placeholder or self.placeholder, "StylePlugin", "en")
for j in range(3):
add_plugin(placeholder or self.placeholder, "StylePlugin", "en", "last-child", parent)
def get_first_root_plugin(self, placeholder=None):
return self.get_plugins(placeholder).filter(parent__isnull=True).first()
def get_last_root_plugin(self, placeholder=None):
return self.get_plugins(placeholder).filter(parent__isnull=True).last()
def get_plugins(self, placeholder=None):
if placeholder is None:
placeholder = self.placeholder
return CMSPlugin.objects.filter(placeholder=placeholder)
def get_plugin_tree(self, placeholder=None):
tree = {}
for root_plugin in self.get_plugins(placeholder).filter(parent__isnull=True):
tree[root_plugin.pk] = self.get_plugin_descendants(root_plugin)
return tree
def get_plugin_descendants(self, plugin):
return list(self._unpack_descendants(plugin))
def assertPluginTreeEquals(self, plugins, placeholder=None):
"""
plugins should be ordered by position
"""
new_tree = self.get_plugins(placeholder).values_list("pk", "position")
expected = [(pk, pos) for pos, pk in enumerate(plugins, 1)]
self.assertSequenceEqual(new_tree, expected)
class PlaceholderFlatPluginTests(PlaceholderPluginTestsBase):
def test_delete(self):
"""
Deletes all root plugins from the plugin tree,
one by one, comparing plugin positions at every iteration.
"""
tree = self.get_plugin_tree()
plugin_tree_all = list(self.get_plugins().values_list("pk", flat=True))
for plugin in self.get_plugins().filter(parent__isnull=True):
for plugin_id in [plugin.pk] + tree[plugin.pk]:
plugin_tree_all.remove(plugin_id)
plugin.refresh_from_db()
self.placeholder.delete_plugin(plugin)
new_tree = self.get_plugins().values_list("pk", "position")
expected = [(pk, pos) for pos, pk in enumerate(plugin_tree_all, 1)]
self.assertSequenceEqual(new_tree, expected)
def test_move_left(self):
"""
Moves the last plugin in the tree to the left,
one step at a time until it reaches the beginning of the tree.
"""
plugin = self.get_last_root_plugin()
plugin_tree = [plugin.pk] + self.get_plugin_descendants(plugin)
plugin_tree_all = list(self.get_plugins().values_list("pk", flat=True))
positions = list(
self.get_plugins()
.filter(parent__isnull=True)
.reverse()
.exclude(pk=plugin.pk)
.values_list("pk", "position")
)
for pk, position in positions:
self.placeholder.move_plugin(plugin, position)
plugin.refresh_from_db()
for edge, plugin_id in enumerate(plugin_tree):
target_index = plugin_tree_all.index(pk)
plugin_tree_all.remove(plugin_id)
plugin_tree_all.insert(target_index, plugin_id)
self.assertPluginTreeEquals(plugin_tree_all)
def test_move_left_middle(self):
tree = {}
root_tree = self.get_plugins().filter(parent__isnull=True)
count = root_tree.count()
first_plugin = root_tree.first()
middle_plugin = root_tree[count // 2]
last_plugin = root_tree.last()
plugin_tree_all = list(self.get_plugins().values_list("pk", flat=True))
for root_plugin in [first_plugin, middle_plugin, last_plugin]:
tree[root_plugin.pk] = list(self._unpack_descendants(root_plugin))
for target_plugin in [middle_plugin, first_plugin]:
self.placeholder.move_plugin(last_plugin, target_plugin.position)
last_plugin.refresh_from_db()
# Assumes(!) that the children follow directly the parents!
for edge, plugin_id in enumerate([last_plugin.pk] + tree[last_plugin.pk]):
target_index = plugin_tree_all.index(target_plugin.pk)
plugin_tree_all.remove(plugin_id)
plugin_tree_all.insert(target_index, plugin_id)
new_tree = self.get_plugins().values_list("pk", "position")
expected = [(pk, pos) for pos, pk in enumerate(plugin_tree_all, 1)]
self.assertSequenceEqual(new_tree, expected)
def test_move_right(self):
tree = self.get_plugin_tree()
plugin = self.get_plugins().first()
plugin_tree_all = list(self.get_plugins().values_list("pk", flat=True))
positions = list(
self.get_plugins().filter(parent__isnull=True).exclude(pk=plugin.pk).values_list("pk", "position")
)
for pk, position in positions:
self.placeholder.move_plugin(plugin, position)
plugin.refresh_from_db()
target_index = plugin_tree_all.index(pk) + len(tree[pk])
for edge, plugin_id in enumerate([plugin.pk] + tree[plugin.pk]):
plugin_tree_all.remove(plugin_id)
plugin_tree_all.insert(target_index, plugin_id)
new_tree = self.get_plugins().values_list("pk", "position")
expected = [(pk, pos) for pos, pk in enumerate(plugin_tree_all, 1)]
self.assertSequenceEqual(new_tree, expected)
def test_move_right_middle(self):
tree = {}
root_tree = self.get_plugins().filter(parent__isnull=True)
count = root_tree.count()
first_plugin = root_tree.first()
middle_plugin = root_tree[count // 2]
last_plugin = root_tree.last()
plugin_tree_all = list(self.get_plugins().values_list("pk", flat=True))
for root_plugin in [first_plugin, middle_plugin, last_plugin]:
tree[root_plugin.pk] = list(self._unpack_descendants(root_plugin))
for target_plugin in [middle_plugin, last_plugin]:
self.placeholder.move_plugin(first_plugin, target_plugin.position)
first_plugin.refresh_from_db()
target_index = plugin_tree_all.index(target_plugin.pk) + len(tree[target_plugin.pk])
for edge, plugin_id in enumerate([first_plugin.pk] + tree[first_plugin.pk]):
plugin_tree_all.remove(plugin_id)
plugin_tree_all.insert(target_index, plugin_id)
new_tree = self.get_plugins().values_list("pk", "position")
expected = [(pk, pos) for pos, pk in enumerate(plugin_tree_all, 1)]
self.assertSequenceEqual(new_tree, expected)
def test_move_to_top(self):
"""
Moves the last plugin in the tree to the top of the tree.
"""
tree = {}
first_plugin = self.get_plugins().filter(parent__isnull=True).first()
last_plugin = self.get_plugins().filter(parent__isnull=True).last()
plugin_tree_all = list(self.get_plugins().values_list("pk", flat=True))
for root_plugin in [first_plugin, last_plugin]:
tree[root_plugin.pk] = list(self._unpack_descendants(root_plugin))
self.placeholder.move_plugin(last_plugin, first_plugin.position)
last_plugin.refresh_from_db()
for edge, plugin_id in enumerate([last_plugin.pk] + tree[last_plugin.pk]):
target_index = plugin_tree_all.index(first_plugin.pk)
plugin_tree_all.remove(plugin_id)
plugin_tree_all.insert(target_index, plugin_id)
new_tree = self.get_plugins().values_list("pk", "position")
expected = [(pk, pos) for pos, pk in enumerate(plugin_tree_all, 1)]
self.assertSequenceEqual(new_tree, expected)
def test_move_to_placeholder_top(self):
source_plugins = self.get_plugins().filter(parent__isnull=True)
source_tree_by_root = self.get_plugin_tree()
source_plugin_tree_all = list(self.get_plugins().values_list("pk", flat=True))
target = self._create_placeholder("target")
self.create_plugins(target)
target_plugin_tree_all = list(self.get_plugins(target).values_list("pk", flat=True))
for plugin in source_plugins:
plugin_tree = [plugin.pk] + source_tree_by_root[plugin.pk]
plugin.refresh_from_db(fields=["position"])
self.placeholder.move_plugin(plugin, 1, target_placeholder=target)
for edge, plugin_id in enumerate(plugin_tree):
source_plugin_tree_all.remove(plugin_id)
target_plugin_tree_all.insert(edge, plugin_id)
self.assertPluginTreeEquals(source_plugin_tree_all)
self.assertPluginTreeEquals(target_plugin_tree_all, placeholder=target)
def test_move_to_bottom(self):
tree = {}
first_plugin = self.get_plugins().first()
last_plugin = self.get_plugins().last()
plugin_tree_all = list(self.get_plugins().values_list("pk", flat=True))
for root_plugin in [first_plugin, last_plugin]:
tree[root_plugin.pk] = list(self._unpack_descendants(root_plugin))
self.placeholder.move_plugin(first_plugin, last_plugin.position)
first_plugin.refresh_from_db()
target_index = plugin_tree_all.index(last_plugin.pk) + len(tree[last_plugin.pk])
for edge, plugin_id in enumerate([first_plugin.pk] + tree[first_plugin.pk]):
plugin_tree_all.remove(plugin_id)
plugin_tree_all.insert(target_index, plugin_id)
new_tree = self.get_plugins().values_list("pk", "position")
expected = [(pk, pos) for pos, pk in enumerate(plugin_tree_all, 1)]
self.assertSequenceEqual(new_tree, expected)
def test_move_to_placeholder_bottom(self):
source_plugins = self.get_plugins().filter(parent__isnull=True)
source_tree_by_root = self.get_plugin_tree()
source_plugin_tree_all = list(self.get_plugins().values_list("pk", flat=True))
target = self._create_placeholder("target")
self.create_plugins(target)
target_plugin_tree_all = list(self.get_plugins(target).values_list("pk", flat=True))
target_position = len(target_plugin_tree_all) + 1
for plugin in source_plugins:
plugin_tree = [plugin.pk] + source_tree_by_root[plugin.pk]
plugin.refresh_from_db(fields=["position"])
self.placeholder.move_plugin(plugin, target_position, target_placeholder=target)
for edge, plugin_id in enumerate(plugin_tree):
# target position is 1 indexed
index = (target_position - 1) + edge
source_plugin_tree_all.remove(plugin_id)
target_plugin_tree_all.insert(index, plugin_id)
target_position += len(plugin_tree)
self.assertPluginTreeEquals(source_plugin_tree_all)
self.assertPluginTreeEquals(target_plugin_tree_all, placeholder=target)
class PlaceholderNestedPluginTests(PlaceholderFlatPluginTests):
"""
Same tests as for PlaceholderFlatPluginTests but now with a different plugin tree:
::
Parent 1
Parent 2
Child
Parent 1
Parent 2
Child
Parent 1
Parent 2
Child
Parent 1
Parent 2
Child
"""
def create_plugins(self, placeholder):
for i in range(1, 12, 3):
parent = self._create_plugin(placeholder, position=i)
parent_2 = self._create_plugin(placeholder, parent=parent, position=i + 1)
self._create_plugin(placeholder, parent=parent_2, position=i + 2)
def test_move_to_placeholder_under_parent(self):
plugin = self.get_plugins().filter(parent__isnull=True).first()
source_tree_by_root = self.get_plugin_tree()
source_plugin_tree_all = list(self.get_plugins().values_list("pk", flat=True))
target = self._create_placeholder("target")
self.create_plugins(target)
# Create a single plugin as the first plugin of the target placeholder
target_plugin = CMSPlugin(language="en", plugin_type="StylePlugin", position=1, placeholder=target)
target_plugin = target.add_plugin(target_plugin)
target_plugin_tree_all = list(self.get_plugins(target).values_list("pk", flat=True))
plugin_tree = [plugin.pk] + source_tree_by_root[plugin.pk]
plugin.refresh_from_db(fields=["position"])
self.placeholder.move_plugin(plugin, 2, target_placeholder=target, target_plugin=target_plugin)
for edge, plugin_id in enumerate(plugin_tree):
source_plugin_tree_all.remove(plugin_id)
target_plugin_tree_all.insert(1 + edge, plugin_id)
self.assertPluginTreeEquals(source_plugin_tree_all)
self.assertPluginTreeEquals(target_plugin_tree_all, placeholder=target)
def test_delete_single(self):
tree = self.get_plugin_tree()
plugin_tree_all = list(self.get_plugins().values_list("pk", flat=True))
for plugin in self.get_plugins().filter(parent__isnull=True):
for plugin_id in [plugin.pk] + tree[plugin.pk]:
plugin_tree_all.remove(plugin_id)
plugin.refresh_from_db()
self.placeholder.delete_plugin(plugin)
new_tree = self.get_plugins().values_list("pk", "position")
expected = [(pk, pos) for pos, pk in enumerate(plugin_tree_all, 1)]
self.assertSequenceEqual(new_tree, expected)
def test_scan_placeholders_basic(self):
"""
Test that _scan_placeholders finds placeholders in a simple template.
"""
template_str = '{% load cms_tags %}{% placeholder "slot2" %}{% placeholder "slot1" %}'
tpl = Template(template_str)
nodes = _scan_placeholders(tpl.nodelist)
slots = [node.get_declaration().slot for node in nodes]
assert slots == ["slot2", "slot1"]
def test_scan_placeholders_include(self):
"""
Test that _scan_placeholders finds placeholders in included templates.
"""
django_engine = engines["django"]
tpl_main = django_engine.from_string('{% load cms_tags %}{% include "included_template.html" %}')
tpl_included = django_engine.from_string('{% load cms_tags %}{% placeholder "included_slot" %}')
# Patch get_template to return our included template
import cms.utils.placeholder as placeholder_utils
orig_get_template = placeholder_utils.get_template
def fake_get_template(name):
if name == "included_template.html":
return tpl_included
return orig_get_template(name)
try:
placeholder_utils.get_template = fake_get_template
nodelist = getattr(tpl_main, "nodelist", getattr(tpl_main, "template", tpl_main).nodelist)
nodes = _scan_placeholders(nodelist)
slots = [node.get_declaration().slot for node in nodes]
self.assertEqual(slots, ["included_slot"])
finally:
placeholder_utils.get_template = orig_get_template
def test_scan_placeholders_extends_and_blocks(self):
"""
Test that _scan_placeholders finds placeholders in extended templates and blocks.
"""
django_engine = engines["django"]
tpl_base = django_engine.from_string(
'{% load cms_tags %}{% block content %}{% placeholder "base_slot" %}{% endblock %}'
)
tpl_child = django_engine.from_string(
'{% extends "base_template.html" %}{% load cms_tags %}{% block content %}{% placeholder "child_slot" %}{{ block.super }}{% endblock %}'
)
orig_find_template = django_engine.engine.find_template
def fake_find_template(name, skip=None):
if name == "base_template.html":
return tpl_base, None
return orig_find_template(name, skip=skip)
try:
django_engine.engine.find_template = fake_find_template
nodelist = getattr(tpl_child, "nodelist", getattr(tpl_child, "template", tpl_child).nodelist)
nodes = _scan_placeholders(nodelist)
slots = [node.get_declaration().slot for node in nodes]
self.assertEqual(sorted(slots), ["base_slot", "child_slot"])
finally:
django_engine.engine.find_template = orig_find_template
def test_scan_placeholders_ignore_variable_include(self):
"""
Edge case: _scan_placeholders should skip includes with variable template names.
"""
template_str = '{% load cms_tags %}{% include some_var %}{% placeholder "slotA" %}'
tpl = Template(template_str)
nodes = _scan_placeholders(getattr(tpl, "nodelist", getattr(tpl, "template", tpl).nodelist))
slots = [node.get_declaration().slot for node in nodes]
assert slots == ["slotA"]
def test_scan_placeholders_duplicate_slots_warns(self):
"""
Edge case: duplicate placeholder slots should raise a warning when using get_placeholders.
"""
template_str = '{% load cms_tags %}{% placeholder "dup_slot" %}{% placeholder "dup_slot" %}'
tpl_dup = Template(template_str)
import cms.utils.placeholder as placeholder_utils
orig_get_template = placeholder_utils.get_template
def fake_get_template(name):
if name == "duplicate_test.html":
return tpl_dup
return orig_get_template(name)
try:
placeholder_utils.get_template = fake_get_template
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
placeholders = get_placeholders("duplicate_test.html")
slots = [pl.slot for pl in placeholders]
# Duplicates should be collapsed to a single slot, warning emitted
self.assertEqual(slots, ["dup_slot"])
self.assertTrue(any(isinstance(warn.message, DuplicatePlaceholderWarning) for warn in w))
finally:
placeholder_utils.get_template = orig_get_template
def test_placeholder_adds_itself_to_added_plugins(self):
"""
When using `Placeholder.add_plugin(p)`, `p.placeholder` should be set as a side effect.
"""
placeholder = Placeholder.objects.create(slot="placeholder")
plugin = CMSPlugin()
placeholder.add_plugin(plugin)
self.assertEqual(plugin.placeholder, placeholder)