Repository: wagtail/wagtail

Issue:
Copying a page with a generated unique field raises a ValidationError 
### Issue Summary

I have a Page model, that has a unique UUID field in it. When I'm trying to copy such a page, I receive a ValidationError saying "this Uuid already exists".  

To be specific, at first, it's getting copied as a draft just fine. But the ValidationError occurs when I'm trying to publish a copy.

After some debugging, I suppose that the existing UUID comes from the copied revision of the original page. I was trying to adjust the copying behaviour, but it seems that there is no control over this process when copying from UI. `Page.copy` method isn't called, so there is no way to pass arguments like `update_attrs` or `copy_revisions=False` into CopyPageAction

I'm using `exclude_fields_in_copy` setting, but it doesn't affect revision's copy process.


### Steps to Reproduce

1. Set up a clean wagtail/bakerydemo project
2. Add the following code to the end of `bakerydemo/blog/models.py` :

``` python
import uuid


class TestBlogPage(Page):
    uuid = models.UUIDField(default=uuid.uuid4, unique=True)
    exclude_fields_in_copy = ['uuid']
    
```
3. Create and run migrations:
```bash
./manage.py makemigrations
./manage.py migrate
```

4. Log in to /admin. Navigate to  `Root -> Welcome to the Wagtail Bakery!`
5. Create a child page of type `Test blog page` under the `Welcome to the Wagtail Bakery!` page and publish.
6. Copy the test page. Ensure "Alias" and "Publish copied page" are not checked. Also, you need to slightly tweak the slug and title fields.
7. Publish the copy. Observe ValidationError
<img width="629" alt="image" src="https://github.com/wagtail/wagtail/assets/3090883/6dc9d643-0baf-420b-8280-7adaa482d584">

---

-   I have confirmed that this issue can be reproduced as described on a fresh Wagtail project: yes

### Technical details

-   Python version: 3.11.3
-   Django version: 4.2.5
-   Wagtail version: 5.1.1



Discussion:
I have given this a go and can see what the cause of this is, I do not think it's specific to Wagtail but how Django creates new instances when `default=...` is set.

I am not sure whether this is a bug that Wagtail can fix, the exclude appears to be working but what's happening is the way that `default=` works is that it runs the function in cases where you may not expect, especially when combined with `unique=True`.

For example - adding a uuid to an existing model is not straight-forward. https://docs.djangoproject.com/en/4.2/howto/writing-migrations/#migrations-that-add-unique-fields (in some ways a Copy of the model instance works similarly).

If you change your uuid field to remove `unique=True`

```py
uuid = models.UUIDField(default=uuid.uuid4)
```

Copying works fine, new copies contain a new UUID (I have done two tests and each one was unique). Obviously this does not solve the hard unique constraint at the DB level. You could try to reach for Django's `unique_together`, this appears to be working OK for me.

```py

    class Meta:
        unique_together = ('uuid',)
```

An alternative approach would be to change where you set the default value, you could do this with a Django Signal or some other method.

I will leave this open for now but I am not sure on the best course of action, it's possible I have missed something in my understanding of this problem or how Django works.


Not sure if you have looked at `exclude_fields_in_copy`, but my app has that set to exclude the UUID field from our page models to get around this issue: https://docs.wagtail.org/en/stable/reference/pages/model_reference.html#wagtail.models.Page.exclude_fields_in_copy

I don't have it set to unique=True, but I did want to ensure a new UUID was generated for copied pages.
@lb- @cyface  I beleive I've found the bug with the `exclude_fields_in_copy` in wagtail 5.2.2 #11428 
It looks to me like that is the cause of the current issue. And that may be easier to debug, as it has nothing to do with `unique` and `default` settings

Resolving patch:
diff --git a/wagtail/actions/copy_page.py b/wagtail/actions/copy_page.py
--- a/wagtail/actions/copy_page.py
+++ b/wagtail/actions/copy_page.py
@@ -221,6 +221,14 @@ def _copy_page(
                                 child_object["translation_key"]
                             )
 
+                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
+                        )
+
                 revision.content = revision_content
 
                 # Save


Test patch:
diff --git a/wagtail/tests/test_page_model.py b/wagtail/tests/test_page_model.py
--- a/wagtail/tests/test_page_model.py
+++ b/wagtail/tests/test_page_model.py
@@ -1909,13 +1909,16 @@ def test_copy_page_with_additional_excluded_fields(self):
                 special_field="Context is for Kings",
             )
         )
+        page.save_revision()
         new_page = page.copy(to=homepage, update_attrs={"slug": "disco-2"})
+        exclude_field = new_page.latest_revision.content["special_field"]
 
         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)
 
     def test_page_with_generic_relation(self):
         """Test that a page with a GenericRelation will have that relation ignored when