Files
old-saburly-wagtail-web/env/lib/python3.10/site-packages/wagtail/tests/test_reference_index.py
2024-08-27 20:33:44 +02:00

481 lines
18 KiB
Python

from io import StringIO
from django.contrib.contenttypes.models import ContentType
from django.core import management
from django.test import TestCase
from django.utils.functional import SimpleLazyObject
from wagtail.blocks import StreamValue, StructValue
from wagtail.documents import get_document_model
from wagtail.documents.tests.utils import get_test_document_file
from wagtail.images import get_image_model
from wagtail.images.tests.utils import get_test_image_file
from wagtail.models import Page, ReferenceIndex
from wagtail.rich_text import RichText
from wagtail.test.testapp.models import (
Advert,
AdvertWithCustomUUIDPrimaryKey,
EventPage,
EventPageCarouselItem,
EventPageRelatedLink,
GenericSnippetNoFieldIndexPage,
GenericSnippetNoIndexPage,
GenericSnippetPage,
ModelWithNullableParentalKey,
VariousOnDeleteModel,
)
class TestCreateOrUpdateForObject(TestCase):
def setUp(self):
image_model = get_image_model()
self.image_content_type = ContentType.objects.get_for_model(image_model)
self.test_feed_image = image_model.objects.create(
title="Test feed image",
file=get_test_image_file(),
)
self.test_image_1 = image_model.objects.create(
title="Test image 1",
file=get_test_image_file(),
)
self.test_image_2 = image_model.objects.create(
title="Test image 2",
file=get_test_image_file(),
)
# Add event page
self.event_page = EventPage(
title="Event page",
slug="event-page",
location="the moon",
audience="public",
cost="free",
date_from="2001-01-01",
feed_image=self.test_feed_image,
)
self.event_page.carousel_items = [
EventPageCarouselItem(
caption="1234567", image=self.test_image_1, sort_order=1
),
EventPageCarouselItem(
caption="7654321", image=self.test_image_2, sort_order=2
),
EventPageCarouselItem(
caption="abcdefg", image=self.test_image_1, sort_order=3
),
]
self.root_page = Page.objects.get(id=2)
self.root_page.add_child(instance=self.event_page)
self.expected_references = {
(
self.image_content_type.id,
str(self.test_feed_image.pk),
"feed_image",
"feed_image",
),
(
self.image_content_type.id,
str(self.test_image_1.pk),
"carousel_items.item.image",
f"carousel_items.{self.event_page.carousel_items.get(sort_order=1).id}.image",
),
(
self.image_content_type.id,
str(self.test_image_2.pk),
"carousel_items.item.image",
f"carousel_items.{self.event_page.carousel_items.get(sort_order=2).id}.image",
),
(
self.image_content_type.id,
str(self.test_image_1.pk),
"carousel_items.item.image",
f"carousel_items.{self.event_page.carousel_items.get(sort_order=3).id}.image",
),
}
def test(self):
self.assertSetEqual(
set(
ReferenceIndex.get_references_for_object(self.event_page).values_list(
"to_content_type", "to_object_id", "model_path", "content_path"
)
),
self.expected_references,
)
def test_update(self):
reference_to_keep = ReferenceIndex.objects.get(
base_content_type=ReferenceIndex._get_base_content_type(self.event_page),
content_type=ContentType.objects.get_for_model(self.event_page),
content_path="feed_image",
)
reference_to_remove = ReferenceIndex.objects.create(
base_content_type=ReferenceIndex._get_base_content_type(self.event_page),
content_type=ContentType.objects.get_for_model(self.event_page),
object_id=self.event_page.pk,
to_content_type=self.image_content_type,
to_object_id=self.test_image_1.pk,
model_path="hero_image", # Field doesn't exist
content_path="hero_image",
content_path_hash=ReferenceIndex._get_content_path_hash("hero_image"),
)
ReferenceIndex.create_or_update_for_object(self.event_page)
# Check that the record for the reference to be kept has been preserved/reused
self.assertTrue(ReferenceIndex.objects.filter(id=reference_to_keep.id).exists())
# Check that the record for the reference to be removed has been deleted
self.assertFalse(
ReferenceIndex.objects.filter(id=reference_to_remove.id).exists()
)
# Check that the current stored references are correct
self.assertSetEqual(
set(
ReferenceIndex.get_references_for_object(self.event_page).values_list(
"to_content_type", "to_object_id", "model_path", "content_path"
)
),
{
(
self.image_content_type.id,
str(self.test_feed_image.pk),
"feed_image",
"feed_image",
),
(
self.image_content_type.id,
str(self.test_image_1.pk),
"carousel_items.item.image",
f"carousel_items.{self.event_page.carousel_items.get(sort_order=1).id}.image",
),
(
self.image_content_type.id,
str(self.test_image_2.pk),
"carousel_items.item.image",
f"carousel_items.{self.event_page.carousel_items.get(sort_order=2).id}.image",
),
(
self.image_content_type.id,
str(self.test_image_1.pk),
"carousel_items.item.image",
f"carousel_items.{self.event_page.carousel_items.get(sort_order=3).id}.image",
),
},
)
def test_saving_base_model_does_not_remove_references(self):
page = Page.objects.get(pk=self.event_page.pk)
page.save()
self.assertSetEqual(
set(
ReferenceIndex.get_references_for_object(self.event_page).values_list(
"to_content_type", "to_object_id", "model_path", "content_path"
)
),
self.expected_references,
)
def test_null_parental_key(self):
obj = ModelWithNullableParentalKey(
content="""<p><a linktype="page" id="%d">event page</a></p>"""
% self.event_page.id
)
obj.save()
# Models with a ParentalKey are not considered indexable - references are recorded against the parent model
# instead. Since the ParentalKey is null here, no reference will be recorded.
refs = ReferenceIndex.get_references_to(self.event_page)
self.assertEqual(refs.count(), 0)
def test_lazy_parental_key(self):
event_page_related_link = EventPageRelatedLink()
# The parent model is a lazy object
event_page_related_link.page = SimpleLazyObject(lambda: self.event_page)
event_page_related_link.link_page = self.root_page
event_page_related_link.save()
refs = ReferenceIndex.get_references_to(self.root_page)
self.assertEqual(refs.count(), 1)
def test_generic_foreign_key(self):
page1 = GenericSnippetPage(
title="generic snippet page", snippet_content_object=self.event_page
)
self.root_page.add_child(instance=page1)
page2 = GenericSnippetPage(
title="generic snippet page", snippet_content_object=None
)
self.root_page.add_child(instance=page2)
refs = ReferenceIndex.get_references_to(self.event_page)
self.assertEqual(refs.count(), 1)
def test_model_index_ignore_generic_foreign_key(self):
page1 = GenericSnippetNoIndexPage(
title="generic snippet page", snippet_content_object=self.event_page
)
self.root_page.add_child(instance=page1)
page2 = GenericSnippetNoIndexPage(
title="generic snippet page", snippet_content_object=None
)
self.root_page.add_child(instance=page2)
# There should be no references
refs = ReferenceIndex.get_references_to(self.event_page)
self.assertEqual(refs.count(), 0)
def test_model_field_index_ignore_generic_foreign_key(self):
content_type = ContentType.objects.get_for_model(self.event_page)
page1 = GenericSnippetNoFieldIndexPage(
title="generic snippet page", snippet_content_type_nonindexed=content_type
)
self.root_page.add_child(instance=page1)
page2 = GenericSnippetNoFieldIndexPage(
title="generic snippet page", snippet_content_type_nonindexed=None
)
self.root_page.add_child(instance=page2)
# There should be no references
refs = ReferenceIndex.get_references_to(content_type)
self.assertEqual(refs.count(), 0)
def test_rebuild_references_index_no_verbosity(self):
stdout = StringIO()
management.call_command(
"rebuild_references_index",
verbosity=0,
stdout=stdout,
)
self.assertFalse(stdout.getvalue())
def test_show_references_index(self):
stdout = StringIO()
management.call_command(
"show_references_index",
stdout=stdout,
)
self.assertIn(" 3 wagtail.images.models.Image", stdout.getvalue())
self.assertIn(" 4 wagtail.test.testapp.models.EventPage", stdout.getvalue())
class TestDescribeOnDelete(TestCase):
fixtures = ["test.json"]
@classmethod
def setUpTestData(cls):
management.call_command("rebuild_references_index", stdout=StringIO())
def setUp(self):
field = VariousOnDeleteModel._meta.get_field("stream_field")
advertisement_content = field.stream_block.child_blocks["advertisement_content"]
captioned_advert = advertisement_content.child_blocks["captioned_advert"]
self.advert = Advert.objects.create(text="An advertisement")
self.advert_uuid = AdvertWithCustomUUIDPrimaryKey.objects.create(
text="A UUID advertisement"
)
self.page = EventPage.objects.first()
page_link = f'<p>Link to <a id="{self.page.id}" linktype="page">a page</a></p>'
self.image = get_image_model().objects.create(
title="My image",
file=get_test_image_file(),
)
self.document = get_document_model().objects.create(
title="My document",
file=get_test_document_file(),
)
# Each case is a tuple of (
# VariousOnDeleteModel init kwargs,
# referred object,
# expected field description,
# expected on delete description,
# )
self.cases = [
# References from ForeignKey
(
{"text": "on_delete=CASCADE", "on_delete_cascade": self.advert},
self.advert,
"On delete cascade",
"the various on delete model will also be deleted",
),
(
{"text": "on_delete=PROTECT", "on_delete_protect": self.advert},
self.advert,
"On delete protect",
"prevents deletion",
),
(
{"text": "on_delete=RESTRICT", "on_delete_restrict": self.advert},
self.advert,
"On delete restrict",
"may prevent deletion",
),
(
{"text": "on_delete=SET_NULL", "on_delete_set_null": self.advert},
self.advert,
"On delete set null",
"will unset the reference",
),
(
{"text": "on_delete=SET_DEFAULT", "on_delete_set_default": self.advert},
self.advert,
"On delete set default",
"will be set to the default various on delete model",
),
(
{"text": "on_delete=SET", "on_delete_set": self.advert},
self.advert,
"On delete set",
"will be set to a various on delete model specified by the system",
),
(
{"text": "on_delete=DO_NOTHING", "on_delete_do_nothing": self.advert},
self.advert,
"On delete do nothing",
"will do nothing",
),
# References from GenericForeignKey
(
{"text": "GenericForeignKey", "content_object": self.advert_uuid},
self.advert_uuid,
"Content object",
"will unset the reference",
),
# References from RichTextField
(
{"text": "RichTextField model field", "rich_text": page_link},
self.page,
"Rich text",
"will unset the reference",
),
(
{
"text": "deep RichTextBlock",
"stream_field": StreamValue(
field.stream_block,
[
(
"advertisement_content",
StreamValue(
advertisement_content,
[
(
"rich_text",
RichText(page_link),
)
],
),
)
],
),
},
self.page,
"Stream field → Advertisement content → Rich text",
"will unset the reference",
),
# References from StreamField
(
{
"text": "deep SnippetChooserBlock",
"stream_field": StreamValue(
field.stream_block,
[
(
"advertisement_content",
StreamValue(
advertisement_content,
[
(
"captioned_advert",
StructValue(
captioned_advert,
[
("advert", self.advert),
("caption", "Deep text"),
],
),
)
],
),
)
],
),
},
self.advert,
"Stream field → Advertisement content → Captioned advert",
"will unset the reference",
),
(
{
"text": "ImageChooserBlock",
"stream_field": StreamValue(
field.stream_block, [("image", self.image)]
),
},
self.image,
"Stream field → Image",
"will unset the reference",
),
(
{
"text": "DocumentChooserBlock",
"stream_field": StreamValue(
field.stream_block, [("document", self.document)]
),
},
self.document,
"Stream field → Document",
"will unset the reference",
),
]
def test_describe_source_field_and_on_delete(self):
for (
init_kwargs,
referred_object,
field_description,
on_delete_description,
) in self.cases:
with self.subTest(test=init_kwargs["text"]):
# Explicitly pass None to this field so that it is not set to
# the default value for test cases other than the SET_DEFAULT case
if "on_delete_set_default" not in init_kwargs:
init_kwargs["on_delete_set_default"] = None
obj = VariousOnDeleteModel.objects.create(**init_kwargs)
usage = ReferenceIndex.get_references_to(
referred_object
).group_by_source_object()
referrer, references = usage[0]
reference = references[0]
self.assertIs(usage.is_protected, "on_delete_protect" in init_kwargs)
self.assertEqual(usage.count(), 1)
self.assertEqual(referrer, obj)
self.assertEqual(len(references), 1)
self.assertEqual(reference.describe_source_field(), field_description)
self.assertEqual(reference.describe_on_delete(), on_delete_description)
obj.delete()
def test_describe_source_field_and_on_delete_parental_key(self):
# The test fixtures contain two references to the advert:
# 1. One advert placement on the home page
# 2. One advert placement on the Christmas page
advert = Advert.objects.first()
usage = ReferenceIndex.get_references_to(advert).group_by_source_object()
self.assertEqual(usage.count(), 2)
for _, references in usage:
reference = references[0]
self.assertEqual(reference.describe_source_field(), "Advert")
self.assertEqual(
reference.describe_on_delete(),
"the advert placement will also be deleted",
)