kopia lustrzana https://github.com/wagtail/wagtail
				
				
				
			Handle non-JSON-safe fields in exclude_fields_in_copy
Fixes #11715 When copying a page's revision history, fields listed in exclude_fields_on_copy are replaced with the new value from the copied page instance (which has already had the exclude_fields_on_copy logic applied by this point, meaning that we'll get the model's default value instead). However, this does not take into account whether the value from the instance is JSON-serializable. This causes it to break when the field is a one-to-many or many-to-many relation, which returns the manager object (#11715), or a field that works with non-JSON-safe values such as StreamField. Also, the old code was using the page model's exclude_fields_on_copy rather than the exclude_fields variable which also includes `default_exclude_fields_in_copy` and the passed-in `exclude_fields` argument - this meant that core fields such as `url_path` were not being replaced as expected.pull/13082/head
							rodzic
							
								
									d1fbb2e262
								
							
						
					
					
						commit
						a2c20062bf
					
				| 
						 | 
				
			
			@ -20,6 +20,7 @@ Changelog
 | 
			
		|||
 * Fix: Ensure `WAGTAILADMIN_LOGIN_URL` is respected when logging out of the admin (Antoine Rodriguez, Ramon de Jezus)
 | 
			
		||||
 * Fix: Fix behaviour of `ViewSet.inject_view_methods` with multiple methods (Gorlik)
 | 
			
		||||
 * Fix: Preserve query strings in URLs submitted to CloudFront for invalidation (Jigyasu Rajput)
 | 
			
		||||
 * Fix: Handle non-JSON-safe fields in `exclude_fields_in_copy` (Matt Westcott)
 | 
			
		||||
 * Maintenance: Refactor `get_embed` to remove `finder` argument which was only used for mocking in unit tests (Jigyasu Rajput)
 | 
			
		||||
 * Maintenance: Simplify handling of `None` values in `TypedTableBlock` (Jigyasu Rajput)
 | 
			
		||||
 * Maintenance: Remove squash.io configuration (Sage Abdullah)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,6 +32,7 @@ depth: 1
 | 
			
		|||
 * Ensure `WAGTAILADMIN_LOGIN_URL` is respected when logging out of the admin (Antoine Rodriguez, Ramon de Jezus)
 | 
			
		||||
 * Fix behaviour of `ViewSet.inject_view_methods` with multiple methods (Gorlik)
 | 
			
		||||
 * Preserve query strings in URLs submitted to CloudFront for invalidation (Jigyasu Rajput)
 | 
			
		||||
 * Handle non-JSON-safe fields in `exclude_fields_in_copy` (Matt Westcott)
 | 
			
		||||
 | 
			
		||||
### Documentation
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -181,6 +181,11 @@ class CopyPageAction:
 | 
			
		|||
 | 
			
		||||
        # Copy revisions
 | 
			
		||||
        if self.copy_revisions:
 | 
			
		||||
            # Fetch data for the new page copy in the same serializable format that would be
 | 
			
		||||
            # written to revisions. Any field in exclude_fields that is found in the revision data
 | 
			
		||||
            # will be replaced with the corresponding field from here.
 | 
			
		||||
            page_copy_data = page_copy.serializable_data()
 | 
			
		||||
 | 
			
		||||
            for revision in page.revisions.all():
 | 
			
		||||
                use_as_latest_revision = revision.pk == page.latest_revision_id
 | 
			
		||||
                revision.pk = None
 | 
			
		||||
| 
						 | 
				
			
			@ -221,13 +226,9 @@ class CopyPageAction:
 | 
			
		|||
                                )
 | 
			
		||||
                            )
 | 
			
		||||
 | 
			
		||||
                for exclude_field in specific_page.exclude_fields_in_copy:
 | 
			
		||||
                    if exclude_field in revision_content and hasattr(
 | 
			
		||||
                        page_copy, exclude_field
 | 
			
		||||
                    ):
 | 
			
		||||
                        revision_content[exclude_field] = getattr(
 | 
			
		||||
                            page_copy, exclude_field, None
 | 
			
		||||
                        )
 | 
			
		||||
                for field_name in exclude_fields:
 | 
			
		||||
                    if field_name in revision_content:
 | 
			
		||||
                        revision_content[field_name] = page_copy_data.get(field_name)
 | 
			
		||||
 | 
			
		||||
                revision.content = revision_content
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,57 @@
 | 
			
		|||
# Generated by Django 5.2.1 on 2025-05-16 10:44
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
import modelcluster.fields
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
import wagtail.fields
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("tests", "0052_variousondeletemodel_protected_page_and_user"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="pagewithexcludedcopyfield",
 | 
			
		||||
            name="special_stream",
 | 
			
		||||
            field=wagtail.fields.StreamField(
 | 
			
		||||
                [("item", 0)],
 | 
			
		||||
                block_lookup={0: ("wagtail.blocks.CharBlock", (), {})},
 | 
			
		||||
                default=[("item", "default item")],
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name="ExcludedCopyPageNote",
 | 
			
		||||
            fields=[
 | 
			
		||||
                (
 | 
			
		||||
                    "id",
 | 
			
		||||
                    models.AutoField(
 | 
			
		||||
                        auto_created=True,
 | 
			
		||||
                        primary_key=True,
 | 
			
		||||
                        serialize=False,
 | 
			
		||||
                        verbose_name="ID",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                (
 | 
			
		||||
                    "sort_order",
 | 
			
		||||
                    models.IntegerField(blank=True, editable=False, null=True),
 | 
			
		||||
                ),
 | 
			
		||||
                ("note", models.CharField(max_length=255)),
 | 
			
		||||
                (
 | 
			
		||||
                    "page",
 | 
			
		||||
                    modelcluster.fields.ParentalKey(
 | 
			
		||||
                        on_delete=django.db.models.deletion.CASCADE,
 | 
			
		||||
                        related_name="special_notes",
 | 
			
		||||
                        to="tests.pagewithexcludedcopyfield",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                "ordering": ["sort_order"],
 | 
			
		||||
                "abstract": False,
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
| 
						 | 
				
			
			@ -240,18 +240,34 @@ class CustomPreviewSizesPage(Page):
 | 
			
		|||
        return "desktop"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ExcludedCopyPageNote(Orderable):
 | 
			
		||||
    page = ParentalKey(
 | 
			
		||||
        "tests.PageWithExcludedCopyField",
 | 
			
		||||
        related_name="special_notes",
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
    )
 | 
			
		||||
    note = models.CharField(max_length=255)
 | 
			
		||||
 | 
			
		||||
    panels = [FieldPanel("note")]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Page with Excluded Fields when copied
 | 
			
		||||
class PageWithExcludedCopyField(Page):
 | 
			
		||||
    content = models.TextField()
 | 
			
		||||
 | 
			
		||||
    # Exclude this field from being copied
 | 
			
		||||
    # Exclude these fields and the special_notes relation from being copied
 | 
			
		||||
    special_field = models.CharField(blank=True, max_length=255, default="Very Special")
 | 
			
		||||
    exclude_fields_in_copy = ["special_field"]
 | 
			
		||||
    special_stream = StreamField(
 | 
			
		||||
        [("item", CharBlock())], default=[("item", "default item")]
 | 
			
		||||
    )
 | 
			
		||||
    exclude_fields_in_copy = ["special_field", "special_notes", "special_stream"]
 | 
			
		||||
 | 
			
		||||
    content_panels = [
 | 
			
		||||
        TitleFieldPanel("title", classname="title"),
 | 
			
		||||
        FieldPanel("special_field"),
 | 
			
		||||
        FieldPanel("content"),
 | 
			
		||||
        FieldPanel("special_stream"),
 | 
			
		||||
        InlinePanel("special_notes", label="Special notes"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,5 @@
 | 
			
		|||
import datetime
 | 
			
		||||
import json
 | 
			
		||||
import unittest
 | 
			
		||||
from unittest.mock import Mock
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -49,6 +50,7 @@ from wagtail.test.testapp.models import (
 | 
			
		|||
    EventIndex,
 | 
			
		||||
    EventPage,
 | 
			
		||||
    EventPageSpeaker,
 | 
			
		||||
    ExcludedCopyPageNote,
 | 
			
		||||
    GenericSnippetPage,
 | 
			
		||||
    ManyToManyBlogPage,
 | 
			
		||||
    MTIBasePage,
 | 
			
		||||
| 
						 | 
				
			
			@ -1994,24 +1996,34 @@ class TestCopyPage(TestCase):
 | 
			
		|||
 | 
			
		||||
    def test_copy_page_with_additional_excluded_fields(self):
 | 
			
		||||
        homepage = Page.objects.get(url_path="/home/")
 | 
			
		||||
        page = homepage.add_child(
 | 
			
		||||
            instance=PageWithExcludedCopyField(
 | 
			
		||||
                title="Discovery",
 | 
			
		||||
                slug="disco",
 | 
			
		||||
                content="NCC-1031",
 | 
			
		||||
                special_field="Context is for Kings",
 | 
			
		||||
            )
 | 
			
		||||
        page = PageWithExcludedCopyField(
 | 
			
		||||
            title="Discovery",
 | 
			
		||||
            slug="disco",
 | 
			
		||||
            content="NCC-1031",
 | 
			
		||||
            special_field="Context is for Kings",
 | 
			
		||||
            special_stream=[("item", "non-default item")],
 | 
			
		||||
        )
 | 
			
		||||
        page.special_notes = [ExcludedCopyPageNote(note="Some note")]
 | 
			
		||||
        homepage.add_child(instance=page)
 | 
			
		||||
        page.save_revision()
 | 
			
		||||
        new_page = page.copy(to=homepage, update_attrs={"slug": "disco-2"})
 | 
			
		||||
        exclude_field = new_page.latest_revision.content["special_field"]
 | 
			
		||||
        revision_content = new_page.latest_revision.content
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(page.title, new_page.title)
 | 
			
		||||
        self.assertNotEqual(page.id, new_page.id)
 | 
			
		||||
        self.assertNotEqual(page.path, new_page.path)
 | 
			
		||||
        # special_field is in the list to be excluded
 | 
			
		||||
        self.assertNotEqual(page.special_field, new_page.special_field)
 | 
			
		||||
        self.assertEqual(new_page.special_field, exclude_field)
 | 
			
		||||
 | 
			
		||||
        # special_field and special_stream are in the list to be excluded,
 | 
			
		||||
        # and should revert to the default
 | 
			
		||||
        self.assertEqual(new_page.special_field, "Very Special")
 | 
			
		||||
        self.assertEqual(revision_content["special_field"], "Very Special")
 | 
			
		||||
        self.assertEqual(new_page.special_stream[0].value, "default item")
 | 
			
		||||
        stream_data = json.loads(revision_content["special_stream"])
 | 
			
		||||
        self.assertEqual(stream_data[0]["value"], "default item")
 | 
			
		||||
 | 
			
		||||
        # The special_notes relation should be cleared on the new page
 | 
			
		||||
        self.assertEqual(new_page.special_notes.count(), 0)
 | 
			
		||||
        self.assertEqual(revision_content["special_notes"], [])
 | 
			
		||||
 | 
			
		||||
    def test_page_with_generic_relation(self):
 | 
			
		||||
        """Test that a page with a GenericRelation will have that relation ignored when
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Ładowanie…
	
		Reference in New Issue