489 lines
18 KiB
Python
489 lines
18 KiB
Python
|
|
import json
|
||
|
|
import unittest
|
||
|
|
|
||
|
|
from django.conf import settings
|
||
|
|
from django.core.exceptions import ImproperlyConfigured
|
||
|
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||
|
|
from django.template import Context, Template
|
||
|
|
from django.test import TestCase
|
||
|
|
|
||
|
|
from wagtail.admin.tests.test_contentstate import content_state_equal
|
||
|
|
from wagtail.models import PAGE_MODEL_CLASSES, Page, Site
|
||
|
|
from wagtail.test.dummy_external_storage import DummyExternalStorage
|
||
|
|
from wagtail.test.testapp.models import (
|
||
|
|
BusinessChild,
|
||
|
|
BusinessIndex,
|
||
|
|
BusinessNowherePage,
|
||
|
|
BusinessSubIndex,
|
||
|
|
EventIndex,
|
||
|
|
EventPage,
|
||
|
|
NoCreatableSubpageTypesPage,
|
||
|
|
NoSubpageTypesPage,
|
||
|
|
SectionedRichTextPage,
|
||
|
|
SimpleChildPage,
|
||
|
|
SimplePage,
|
||
|
|
SimpleParentPage,
|
||
|
|
StreamPage,
|
||
|
|
)
|
||
|
|
from wagtail.test.utils import WagtailPageTests, WagtailTestUtils
|
||
|
|
from wagtail.test.utils.form_data import (
|
||
|
|
inline_formset,
|
||
|
|
nested_form_data,
|
||
|
|
rich_text,
|
||
|
|
streamfield,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class TestAssertTagInHTML(WagtailTestUtils, TestCase):
|
||
|
|
def test_assert_tag_in_html(self):
|
||
|
|
haystack = """<ul>
|
||
|
|
<li class="normal">hugh</li>
|
||
|
|
<li class="normal">pugh</li>
|
||
|
|
<li class="really important" lang="en"><em>barney</em> mcgrew</li>
|
||
|
|
</ul>"""
|
||
|
|
self.assertTagInHTML('<li lang="en" class="important really">', haystack)
|
||
|
|
self.assertTagInHTML('<li class="normal">', haystack, count=2)
|
||
|
|
|
||
|
|
with self.assertRaises(AssertionError):
|
||
|
|
self.assertTagInHTML('<div lang="en" class="important really">', haystack)
|
||
|
|
with self.assertRaises(AssertionError):
|
||
|
|
self.assertTagInHTML(
|
||
|
|
'<li lang="en" class="important really">', haystack, count=2
|
||
|
|
)
|
||
|
|
with self.assertRaises(AssertionError):
|
||
|
|
self.assertTagInHTML('<li lang="en" class="important">', haystack)
|
||
|
|
with self.assertRaises(AssertionError):
|
||
|
|
self.assertTagInHTML(
|
||
|
|
'<li lang="en" class="important really" data-extra="boom">', haystack
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_assert_tag_in_html_with_extra_attrs(self):
|
||
|
|
haystack = """<ul>
|
||
|
|
<li class="normal">hugh</li>
|
||
|
|
<li class="normal">pugh</li>
|
||
|
|
<li class="really important" lang="en"><em>barney</em> mcgrew</li>
|
||
|
|
</ul>"""
|
||
|
|
self.assertTagInHTML(
|
||
|
|
'<li class="important really">', haystack, allow_extra_attrs=True
|
||
|
|
)
|
||
|
|
self.assertTagInHTML("<li>", haystack, count=3, allow_extra_attrs=True)
|
||
|
|
|
||
|
|
with self.assertRaises(AssertionError):
|
||
|
|
self.assertTagInHTML(
|
||
|
|
'<li class="normal" lang="en">', haystack, allow_extra_attrs=True
|
||
|
|
)
|
||
|
|
with self.assertRaises(AssertionError):
|
||
|
|
self.assertTagInHTML(
|
||
|
|
'<li class="important really">',
|
||
|
|
haystack,
|
||
|
|
count=2,
|
||
|
|
allow_extra_attrs=True,
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_assert_tag_in_template_script(self):
|
||
|
|
haystack = """<html>
|
||
|
|
<script type="text/template">
|
||
|
|
<p class="really important">first template block</p>
|
||
|
|
</script>
|
||
|
|
<script type="text/template">
|
||
|
|
<p class="really important">second template block</p>
|
||
|
|
</script>
|
||
|
|
<p class="normal">not in a script tag</p>
|
||
|
|
</html>"""
|
||
|
|
|
||
|
|
self.assertTagInTemplateScript('<p class="important really">', haystack)
|
||
|
|
self.assertTagInTemplateScript(
|
||
|
|
'<p class="important really">', haystack, count=2
|
||
|
|
)
|
||
|
|
|
||
|
|
with self.assertRaises(AssertionError):
|
||
|
|
self.assertTagInTemplateScript('<p class="normal">', haystack)
|
||
|
|
|
||
|
|
|
||
|
|
class TestWagtailPageTests(WagtailPageTests):
|
||
|
|
def setUp(self):
|
||
|
|
super().setUp()
|
||
|
|
site = Site.objects.get(is_default_site=True)
|
||
|
|
self.root = site.root_page.specific
|
||
|
|
|
||
|
|
def test_assert_can_create_at(self):
|
||
|
|
# It should be possible to create an EventPage under an EventIndex,
|
||
|
|
self.assertCanCreateAt(EventIndex, EventPage)
|
||
|
|
self.assertCanCreateAt(Page, EventIndex)
|
||
|
|
# It should not be possible to create a SimplePage under a BusinessChild
|
||
|
|
self.assertCanNotCreateAt(SimplePage, BusinessChild)
|
||
|
|
|
||
|
|
# This should raise, as it *is not* possible
|
||
|
|
with self.assertRaises(AssertionError):
|
||
|
|
self.assertCanCreateAt(SimplePage, BusinessChild)
|
||
|
|
# This should raise, as it *is* possible
|
||
|
|
with self.assertRaises(AssertionError):
|
||
|
|
self.assertCanNotCreateAt(EventIndex, EventPage)
|
||
|
|
|
||
|
|
def test_assert_can_create(self):
|
||
|
|
self.assertFalse(EventIndex.objects.exists())
|
||
|
|
self.assertCanCreate(
|
||
|
|
self.root,
|
||
|
|
EventIndex,
|
||
|
|
{
|
||
|
|
"title": "Event Index",
|
||
|
|
"intro": """{"entityMap": {},"blocks": [
|
||
|
|
{"inlineStyleRanges": [], "text": "Event intro", "depth": 0, "type": "unstyled", "key": "00000", "entityRanges": []}
|
||
|
|
]}""",
|
||
|
|
},
|
||
|
|
)
|
||
|
|
self.assertTrue(EventIndex.objects.exists())
|
||
|
|
self.assertTrue(EventIndex.objects.get().live)
|
||
|
|
|
||
|
|
self.assertCanCreate(
|
||
|
|
self.root,
|
||
|
|
StreamPage,
|
||
|
|
{
|
||
|
|
"title": "Flierp",
|
||
|
|
"body-0-type": "text",
|
||
|
|
"body-0-value": "Dit is onze mooie text",
|
||
|
|
"body-0-order": "0",
|
||
|
|
"body-0-deleted": "",
|
||
|
|
"body-1-type": "rich_text",
|
||
|
|
"body-1-value": """{"entityMap": {},"blocks": [
|
||
|
|
{"inlineStyleRanges": [], "text": "Dit is onze mooie text in een ferrari", "depth": 0, "type": "unstyled", "key": "00000", "entityRanges": []}
|
||
|
|
]}""",
|
||
|
|
"body-1-order": "1",
|
||
|
|
"body-1-deleted": "",
|
||
|
|
"body-2-type": "product",
|
||
|
|
"body-2-value-name": "pegs",
|
||
|
|
"body-2-value-price": "a pound",
|
||
|
|
"body-2-order": "2",
|
||
|
|
"body-2-deleted": "",
|
||
|
|
"body-count": "3",
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
self.assertCanCreate(
|
||
|
|
self.root,
|
||
|
|
SectionedRichTextPage,
|
||
|
|
{
|
||
|
|
"title": "Fight Club",
|
||
|
|
"sections-TOTAL_FORMS": "2",
|
||
|
|
"sections-INITIAL_FORMS": "0",
|
||
|
|
"sections-MIN_NUM_FORMS": "0",
|
||
|
|
"sections-MAX_NUM_FORMS": "1000",
|
||
|
|
"sections-0-body": """{"entityMap": {},"blocks": [
|
||
|
|
{"inlineStyleRanges": [], "text": "Rule 1: You do not talk about Fight Club", "depth": 0, "type": "unstyled", "key": "00000", "entityRanges": []}
|
||
|
|
]}""",
|
||
|
|
"sections-0-ORDER": "0",
|
||
|
|
"sections-0-DELETE": "",
|
||
|
|
"sections-1-body": """{"entityMap": {},"blocks": [
|
||
|
|
{"inlineStyleRanges": [], "text": "Rule 2: You DO NOT talk about Fight Club", "depth": 0, "type": "unstyled", "key": "00000", "entityRanges": []}
|
||
|
|
]}""",
|
||
|
|
"sections-1-ORDER": "0",
|
||
|
|
"sections-1-DELETE": "",
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_assert_can_create_for_page_without_publish(self):
|
||
|
|
self.assertCanCreate(
|
||
|
|
self.root,
|
||
|
|
SimplePage,
|
||
|
|
{"title": "Simple Lorem Page", "content": "Lorem ipsum dolor sit amet"},
|
||
|
|
publish=False,
|
||
|
|
)
|
||
|
|
created_page = Page.objects.get(title="Simple Lorem Page")
|
||
|
|
self.assertFalse(created_page.live)
|
||
|
|
|
||
|
|
def test_assert_can_create_with_form_helpers(self):
|
||
|
|
# same as test_assert_can_create, but using the helpers from wagtail.test.utils.form_data
|
||
|
|
# as an end-to-end test
|
||
|
|
self.assertFalse(EventIndex.objects.exists())
|
||
|
|
self.assertCanCreate(
|
||
|
|
self.root,
|
||
|
|
EventIndex,
|
||
|
|
nested_form_data(
|
||
|
|
{"title": "Event Index", "intro": rich_text("<p>Event intro</p>")}
|
||
|
|
),
|
||
|
|
)
|
||
|
|
self.assertTrue(EventIndex.objects.exists())
|
||
|
|
|
||
|
|
self.assertCanCreate(
|
||
|
|
self.root,
|
||
|
|
StreamPage,
|
||
|
|
nested_form_data(
|
||
|
|
{
|
||
|
|
"title": "Flierp",
|
||
|
|
"body": streamfield(
|
||
|
|
[
|
||
|
|
("text", "Dit is onze mooie text"),
|
||
|
|
(
|
||
|
|
"rich_text",
|
||
|
|
rich_text(
|
||
|
|
"<p>Dit is onze mooie text in een ferrari</p>"
|
||
|
|
),
|
||
|
|
),
|
||
|
|
("product", {"name": "pegs", "price": "a pound"}),
|
||
|
|
]
|
||
|
|
),
|
||
|
|
}
|
||
|
|
),
|
||
|
|
)
|
||
|
|
|
||
|
|
self.assertCanCreate(
|
||
|
|
self.root,
|
||
|
|
SectionedRichTextPage,
|
||
|
|
nested_form_data(
|
||
|
|
{
|
||
|
|
"title": "Fight Club",
|
||
|
|
"sections": inline_formset(
|
||
|
|
[
|
||
|
|
{
|
||
|
|
"body": rich_text(
|
||
|
|
"<p>Rule 1: You do not talk about Fight Club</p>"
|
||
|
|
)
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"body": rich_text(
|
||
|
|
"<p>Rule 2: You DO NOT talk about Fight Club</p>"
|
||
|
|
)
|
||
|
|
},
|
||
|
|
]
|
||
|
|
),
|
||
|
|
}
|
||
|
|
),
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_assert_can_create_subpage_rules(self):
|
||
|
|
simple_page = SimplePage(title="Simple Page", slug="simple", content="hello")
|
||
|
|
self.root.add_child(instance=simple_page)
|
||
|
|
# This should raise an error, as a BusinessChild can not be created under a SimplePage
|
||
|
|
with self.assertRaisesRegex(
|
||
|
|
AssertionError,
|
||
|
|
r"Can not create a tests.businesschild under a tests.simplepage",
|
||
|
|
):
|
||
|
|
self.assertCanCreate(simple_page, BusinessChild, {})
|
||
|
|
|
||
|
|
def test_assert_can_create_validation_error(self):
|
||
|
|
# This should raise some validation errors, complaining about missing
|
||
|
|
# title and slug fields
|
||
|
|
with self.assertRaisesRegex(AssertionError, r"\bslug:\n[\s\S]*\btitle:\n"):
|
||
|
|
self.assertCanCreate(self.root, SimplePage, {})
|
||
|
|
|
||
|
|
def test_assert_allowed_subpage_types(self):
|
||
|
|
self.assertAllowedSubpageTypes(BusinessIndex, {BusinessChild, BusinessSubIndex})
|
||
|
|
self.assertAllowedSubpageTypes(BusinessChild, {})
|
||
|
|
# All page types can be created under the Page model, except those with a parent_page_types
|
||
|
|
# rule excluding it
|
||
|
|
all_but_business = set(PAGE_MODEL_CLASSES) - {
|
||
|
|
BusinessSubIndex,
|
||
|
|
BusinessChild,
|
||
|
|
BusinessNowherePage,
|
||
|
|
SimpleChildPage,
|
||
|
|
}
|
||
|
|
self.assertAllowedSubpageTypes(Page, all_but_business)
|
||
|
|
with self.assertRaises(AssertionError):
|
||
|
|
self.assertAllowedSubpageTypes(
|
||
|
|
BusinessSubIndex, {BusinessSubIndex, BusinessChild}
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_assert_allowed_parent_page_types(self):
|
||
|
|
self.assertAllowedParentPageTypes(
|
||
|
|
BusinessChild, {BusinessIndex, BusinessSubIndex}
|
||
|
|
)
|
||
|
|
self.assertAllowedParentPageTypes(BusinessSubIndex, {BusinessIndex})
|
||
|
|
# BusinessIndex can be created under all page types that do not have a subpage_types rule
|
||
|
|
all_but_business = set(PAGE_MODEL_CLASSES) - {
|
||
|
|
BusinessSubIndex,
|
||
|
|
BusinessChild,
|
||
|
|
BusinessIndex,
|
||
|
|
NoCreatableSubpageTypesPage,
|
||
|
|
NoSubpageTypesPage,
|
||
|
|
SimpleParentPage,
|
||
|
|
}
|
||
|
|
self.assertAllowedParentPageTypes(BusinessIndex, all_but_business)
|
||
|
|
with self.assertRaises(AssertionError):
|
||
|
|
self.assertAllowedParentPageTypes(
|
||
|
|
BusinessSubIndex, {BusinessSubIndex, BusinessIndex}
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class TestFormDataHelpers(TestCase):
|
||
|
|
def test_nested_form_data(self):
|
||
|
|
result = nested_form_data(
|
||
|
|
{
|
||
|
|
"foo": "bar",
|
||
|
|
"parent": {
|
||
|
|
"child": "field",
|
||
|
|
},
|
||
|
|
}
|
||
|
|
)
|
||
|
|
self.assertEqual(result, {"foo": "bar", "parent-child": "field"})
|
||
|
|
|
||
|
|
def test_streamfield(self):
|
||
|
|
result = nested_form_data(
|
||
|
|
{
|
||
|
|
"content": streamfield(
|
||
|
|
[
|
||
|
|
("text", "Hello, world"),
|
||
|
|
("text", "Goodbye, world"),
|
||
|
|
("coffee", {"type": "latte", "milk": "soya"}),
|
||
|
|
]
|
||
|
|
)
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
self.assertEqual(
|
||
|
|
result,
|
||
|
|
{
|
||
|
|
"content-count": "3",
|
||
|
|
"content-0-type": "text",
|
||
|
|
"content-0-value": "Hello, world",
|
||
|
|
"content-0-order": "0",
|
||
|
|
"content-0-deleted": "",
|
||
|
|
"content-1-type": "text",
|
||
|
|
"content-1-value": "Goodbye, world",
|
||
|
|
"content-1-order": "1",
|
||
|
|
"content-1-deleted": "",
|
||
|
|
"content-2-type": "coffee",
|
||
|
|
"content-2-value-type": "latte",
|
||
|
|
"content-2-value-milk": "soya",
|
||
|
|
"content-2-order": "2",
|
||
|
|
"content-2-deleted": "",
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_inline_formset(self):
|
||
|
|
result = nested_form_data(
|
||
|
|
{
|
||
|
|
"lines": inline_formset(
|
||
|
|
[
|
||
|
|
{"text": "Hello"},
|
||
|
|
{"text": "World"},
|
||
|
|
]
|
||
|
|
)
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
self.assertEqual(
|
||
|
|
result,
|
||
|
|
{
|
||
|
|
"lines-TOTAL_FORMS": "2",
|
||
|
|
"lines-INITIAL_FORMS": "0",
|
||
|
|
"lines-MIN_NUM_FORMS": "0",
|
||
|
|
"lines-MAX_NUM_FORMS": "1000",
|
||
|
|
"lines-0-text": "Hello",
|
||
|
|
"lines-0-ORDER": "0",
|
||
|
|
"lines-0-DELETE": "",
|
||
|
|
"lines-1-text": "World",
|
||
|
|
"lines-1-ORDER": "1",
|
||
|
|
"lines-1-DELETE": "",
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_default_rich_text(self):
|
||
|
|
result = rich_text("<h2>title</h2><p>para</p>")
|
||
|
|
self.assertTrue(
|
||
|
|
content_state_equal(
|
||
|
|
json.loads(result),
|
||
|
|
{
|
||
|
|
"entityMap": {},
|
||
|
|
"blocks": [
|
||
|
|
{
|
||
|
|
"inlineStyleRanges": [],
|
||
|
|
"text": "title",
|
||
|
|
"depth": 0,
|
||
|
|
"type": "header-two",
|
||
|
|
"key": "00000",
|
||
|
|
"entityRanges": [],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"inlineStyleRanges": [],
|
||
|
|
"text": "para",
|
||
|
|
"depth": 0,
|
||
|
|
"type": "unstyled",
|
||
|
|
"key": "00000",
|
||
|
|
"entityRanges": [],
|
||
|
|
},
|
||
|
|
],
|
||
|
|
},
|
||
|
|
)
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_rich_text_with_custom_features(self):
|
||
|
|
# feature list doesn't allow <h2>, so it should become an unstyled paragraph block
|
||
|
|
result = rich_text("<h2>title</h2><p>para</p>", features=["p"])
|
||
|
|
self.assertTrue(
|
||
|
|
content_state_equal(
|
||
|
|
json.loads(result),
|
||
|
|
{
|
||
|
|
"entityMap": {},
|
||
|
|
"blocks": [
|
||
|
|
{
|
||
|
|
"inlineStyleRanges": [],
|
||
|
|
"text": "title",
|
||
|
|
"depth": 0,
|
||
|
|
"type": "unstyled",
|
||
|
|
"key": "00000",
|
||
|
|
"entityRanges": [],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"inlineStyleRanges": [],
|
||
|
|
"text": "para",
|
||
|
|
"depth": 0,
|
||
|
|
"type": "unstyled",
|
||
|
|
"key": "00000",
|
||
|
|
"entityRanges": [],
|
||
|
|
},
|
||
|
|
],
|
||
|
|
},
|
||
|
|
)
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_rich_text_with_alternative_editor(self):
|
||
|
|
result = rich_text("<h2>title</h2><p>para</p>", editor="custom")
|
||
|
|
self.assertEqual(result, "<h2>title</h2><p>para</p>")
|
||
|
|
|
||
|
|
|
||
|
|
class TestDummyExternalStorage(WagtailTestUtils, TestCase):
|
||
|
|
def test_save_with_incorrect_file_object_position(self):
|
||
|
|
"""
|
||
|
|
Test that DummyExternalStorage correctly warns about attempts
|
||
|
|
to write files that are not rewound to the start
|
||
|
|
"""
|
||
|
|
# This is a 1x1 black png
|
||
|
|
png = (
|
||
|
|
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00"
|
||
|
|
b"\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00"
|
||
|
|
b"\x1f\x15\xc4\x89\x00\x00\x00\rIDATx\x9cc````"
|
||
|
|
b"\x00\x00\x00\x05\x00\x01\xa5\xf6E@\x00\x00"
|
||
|
|
b"\x00\x00IEND\xaeB`\x82"
|
||
|
|
)
|
||
|
|
simple_png = SimpleUploadedFile(
|
||
|
|
name="test.png", content=png, content_type="image/png"
|
||
|
|
)
|
||
|
|
simple_png.read()
|
||
|
|
with self.assertRaisesMessage(
|
||
|
|
ValueError,
|
||
|
|
"Content file pointer should be at 0 - got 70 instead",
|
||
|
|
):
|
||
|
|
DummyExternalStorage().save("test.png", simple_png)
|
||
|
|
|
||
|
|
|
||
|
|
@unittest.skipUnless(
|
||
|
|
settings.WAGTAIL_CHECK_TEMPLATE_NUMBER_FORMAT,
|
||
|
|
"Number formatting functions have not been patched",
|
||
|
|
)
|
||
|
|
class TestPatchedNumberFormat(TestCase):
|
||
|
|
def test_outputting_number_directly_is_disallowed(self):
|
||
|
|
context = Context({"num": 42})
|
||
|
|
template = Template("the answer is {{ num }}")
|
||
|
|
with self.assertRaises(ImproperlyConfigured):
|
||
|
|
template.render(context)
|
||
|
|
|
||
|
|
def test_outputting_number_via_intcomma(self):
|
||
|
|
context = Context({"num": 9000})
|
||
|
|
template = Template("{% load wagtailadmin_tags %}It's over {{ num|intcomma }}!")
|
||
|
|
self.assertEqual(template.render(context), "It's over 9,000!")
|
||
|
|
|
||
|
|
def test_outputting_number_via_unlocalize(self):
|
||
|
|
context = Context({"num": 9000})
|
||
|
|
template = Template("{% load l10n %}It's over {{ num|unlocalize }}!")
|
||
|
|
self.assertEqual(template.render(context), "It's over 9000!")
|