Repository: wagtail/wagtail

Issue:
copy revisions breaks the editor
When I copy a page through the admin interface and then try to edit it, all the changes are made to the original instead of the copy.

The same happens if I create the new page by calling page.copy().  However, if I call page.copy(copy_revisions=False) then it works as expected.

It looks like the revision.content_json isn't being updated to reflect the new pk.  Then when edit calls Page.get_latest_revision_as_page() it's creating an object that points to the original pk.  This means all the edit form point to the original for saving.  

Also child_relations in the content_json aren't being updated either so all inline panels have the same issue.



Discussion:
Thanks for the report and the in-depth investigation! Sounds like this is the same bug that was previously reported here - https://groups.google.com/d/msg/wagtail/ppT4W0Md0n8/Uxn3yEGO2s8J


Resolving patch:
diff --git a/wagtail/wagtailadmin/views/pages.py b/wagtail/wagtailadmin/views/pages.py
--- a/wagtail/wagtailadmin/views/pages.py
+++ b/wagtail/wagtailadmin/views/pages.py
@@ -665,11 +665,9 @@ def copy(request, page_id):
                     'slug': form.cleaned_data['new_slug'],
                 },
                 keep_live=(can_publish and form.cleaned_data.get('publish_copies')),
+                user=request.user,
             )
 
-            # Assign user of this request as the owner of all the new pages
-            new_page.get_descendants(inclusive=True).update(owner=request.user)
-
             # Give a success message back to the user
             if form.cleaned_data.get('copy_subpages'):
                 messages.success(request, _("Page '{0}' and {1} subpages copied.").format(page.title, new_page.get_descendants().count()))
diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py
--- a/wagtail/wagtailcore/models.py
+++ b/wagtail/wagtailcore/models.py
@@ -1,5 +1,6 @@
 import logging
 import warnings
+import json
 
 import six
 from six import StringIO
@@ -729,7 +730,7 @@ def move(self, target, pos=None):
         # Log
         logger.info("Page moved: \"%s\" id=%d path=%s", self.title, self.id, new_url_path)
 
-    def copy(self, recursive=False, to=None, update_attrs=None, copy_revisions=True, keep_live=True):
+    def copy(self, recursive=False, to=None, update_attrs=None, copy_revisions=True, keep_live=True, user=None):
         # Make a copy
         page_copy = Page.objects.get(id=self.id).specific
         page_copy.pk = None
@@ -742,6 +743,9 @@ def copy(self, recursive=False, to=None, update_attrs=None, copy_revisions=True,
             page_copy.live = False
             page_copy.has_unpublished_changes = True
 
+        if user:
+            page_copy.owner = user
+
         if update_attrs:
             for field, value in update_attrs.items():
                 setattr(page_copy, field, value)
@@ -770,15 +774,47 @@ def copy(self, recursive=False, to=None, update_attrs=None, copy_revisions=True,
                 revision.submitted_for_moderation = False
                 revision.approved_go_live_at = None
                 revision.page = page_copy
+
+                # Update ID fields in content
+                revision_content = json.loads(revision.content_json)
+                revision_content['pk'] = page_copy.pk
+
+                for child_relation in get_all_child_relations(specific_self):
+                    try:
+                        child_objects = revision_content[child_relation.get_accessor_name()]
+                    except KeyError:
+                        # KeyErrors are possible if the revision was created
+                        # before this child relation was added to the database
+                        continue
+
+                    for child_object in child_objects:
+                        child_object[child_relation.field.name] = page_copy.pk
+
+                revision.content_json = json.dumps(revision_content)
+
+                # Save
                 revision.save()
 
+        # Create a new revision
+        # This code serves a few purposes:
+        # * It makes sure update_attrs gets applied to the latest revision so the changes are reflected in the editor
+        # * It bumps the last_revision_created_at value so the new page gets ordered as if it was just created
+        # * It sets the user of the new revision so it's possible to see who copied the page by looking at its history
+        latest_revision = page_copy.get_latest_revision_as_page()
+
+        if update_attrs:
+            for field, value in update_attrs.items():
+                setattr(latest_revision, field, value)
+
+        latest_revision.save_revision(user=user)
+
         # Log
         logger.info("Page copied: \"%s\" id=%d from=%d", page_copy.title, page_copy.id, self.id)
 
         # Copy child pages
         if recursive:
             for child_page in self.get_children():
-                child_page.specific.copy(recursive=True, to=page_copy, copy_revisions=copy_revisions, keep_live=keep_live)
+                child_page.specific.copy(recursive=True, to=page_copy, copy_revisions=copy_revisions, keep_live=keep_live, user=user)
 
         return page_copy
 


Test patch:
diff --git a/wagtail/wagtailcore/tests/test_page_model.py b/wagtail/wagtailcore/tests/test_page_model.py
--- a/wagtail/wagtailcore/tests/test_page_model.py
+++ b/wagtail/wagtailcore/tests/test_page_model.py
@@ -1,5 +1,6 @@
 import warnings
 import datetime
+import json
 
 import pytz
 
@@ -7,6 +8,7 @@
 from django.test.utils import override_settings
 from django.http import HttpRequest, Http404
 from django.contrib.contenttypes.models import ContentType
+from django.contrib.auth import get_user_model
 
 from wagtail.wagtailcore.models import Page, Site
 from wagtail.tests.models import EventPage, EventIndex, SimplePage, PageWithOldStyleRouteMethod, BusinessIndex, BusinessSubIndex, BusinessChild, StandardIndex
@@ -425,11 +427,23 @@ def test_copy_page_copies_revisions(self):
         new_christmas_event = christmas_event.copy(update_attrs={'title': "New christmas event", 'slug': 'new-christmas-event'})
 
         # Check that the revisions were copied
-        self.assertEqual(new_christmas_event.revisions.count(), 1, "Revisions weren't copied")
+        # Copying creates a new revision so we're expecting the new page to have two revisions
+        self.assertEqual(new_christmas_event.revisions.count(), 2)
 
         # Check that the revisions weren't removed from old page
         self.assertEqual(christmas_event.revisions.count(), 1, "Revisions were removed from the original page")
 
+        # Check that the attributes were updated in the latest revision
+        latest_revision = new_christmas_event.get_latest_revision_as_page()
+        self.assertEqual(latest_revision.title, "New christmas event")
+        self.assertEqual(latest_revision.slug, 'new-christmas-event')
+
+        # Check that the ids within the revision were updated correctly
+        new_revision = new_christmas_event.revisions.first()
+        new_revision_content = json.loads(new_revision.content_json)
+        self.assertEqual(new_revision_content['pk'], new_christmas_event.id)
+        self.assertEqual(new_revision_content['speakers'][0]['page'], new_christmas_event.id)
+
     def test_copy_page_copies_revisions_and_doesnt_submit_for_moderation(self):
         christmas_event = EventPage.objects.get(url_path='/home/events/christmas/')
         christmas_event.save_revision(submitted_for_moderation=True)
@@ -438,10 +452,10 @@ def test_copy_page_copies_revisions_and_doesnt_submit_for_moderation(self):
         new_christmas_event = christmas_event.copy(update_attrs={'title': "New christmas event", 'slug': 'new-christmas-event'})
 
         # Check that the old revision is still submitted for moderation
-        self.assertTrue(christmas_event.revisions.first().submitted_for_moderation)
+        self.assertTrue(christmas_event.revisions.order_by('created_at').first().submitted_for_moderation)
 
         # Check that the new revision is not submitted for moderation
-        self.assertFalse(new_christmas_event.revisions.first().submitted_for_moderation)
+        self.assertFalse(new_christmas_event.revisions.order_by('created_at').first().submitted_for_moderation)
 
     def test_copy_page_copies_revisions_and_doesnt_change_created_at(self):
         christmas_event = EventPage.objects.get(url_path='/home/events/christmas/')
@@ -456,8 +470,8 @@ def test_copy_page_copies_revisions_and_doesnt_change_created_at(self):
         new_christmas_event = christmas_event.copy(update_attrs={'title': "New christmas event", 'slug': 'new-christmas-event'})
 
         # Check that the created_at time is the same
-        christmas_event_created_at = christmas_event.get_latest_revision().created_at
-        new_christmas_event_created_at = new_christmas_event.get_latest_revision().created_at
+        christmas_event_created_at = christmas_event.revisions.order_by('created_at').first().created_at
+        new_christmas_event_created_at = new_christmas_event.revisions.order_by('created_at').first().created_at
         self.assertEqual(christmas_event_created_at, new_christmas_event_created_at)
 
     def test_copy_page_copies_revisions_and_doesnt_schedule(self):
@@ -468,10 +482,10 @@ def test_copy_page_copies_revisions_and_doesnt_schedule(self):
         new_christmas_event = christmas_event.copy(update_attrs={'title': "New christmas event", 'slug': 'new-christmas-event'})
 
         # Check that the old revision is still scheduled
-        self.assertEqual(christmas_event.revisions.first().approved_go_live_at, datetime.datetime(2014, 9, 16, 9, 12, 00, tzinfo=pytz.utc))
+        self.assertEqual(christmas_event.revisions.order_by('created_at').first().approved_go_live_at, datetime.datetime(2014, 9, 16, 9, 12, 00, tzinfo=pytz.utc))
 
         # Check that the new revision is not scheduled
-        self.assertEqual(new_christmas_event.revisions.first().approved_go_live_at, None)
+        self.assertEqual(new_christmas_event.revisions.order_by('created_at').first().approved_go_live_at, None)
 
     def test_copy_page_doesnt_copy_revisions_if_told_not_to_do_so(self):
         christmas_event = EventPage.objects.get(url_path='/home/events/christmas/')
@@ -481,7 +495,8 @@ def test_copy_page_doesnt_copy_revisions_if_told_not_to_do_so(self):
         new_christmas_event = christmas_event.copy(update_attrs={'title': "New christmas event", 'slug': 'new-christmas-event'}, copy_revisions=False)
 
         # Check that the revisions weren't copied
-        self.assertEqual(new_christmas_event.revisions.count(), 0, "Revisions were copied")
+        # Copying creates a new revision so we're expecting the new page to have one revision
+        self.assertEqual(new_christmas_event.revisions.count(), 1)
 
         # Check that the revisions weren't removed from old page
         self.assertEqual(christmas_event.revisions.count(), 1, "Revisions were removed from the original page")
@@ -544,7 +559,8 @@ def test_copy_page_copies_recursively_with_revisions(self):
         new_christmas_event = new_events_index.get_children().filter(slug='christmas').first()
 
         # Check that the revisions were copied
-        self.assertEqual(new_christmas_event.specific.revisions.count(), 1, "Revisions weren't copied")
+        # Copying creates a new revision so we're expecting the new page to have two revisions
+        self.assertEqual(new_christmas_event.specific.revisions.count(), 2)
 
         # Check that the revisions weren't removed from old page
         self.assertEqual(old_christmas_event.specific.revisions.count(), 1, "Revisions were removed from the original page")
@@ -561,11 +577,29 @@ def test_copy_page_copies_recursively_but_doesnt_copy_revisions_if_told_not_to_d
         new_christmas_event = new_events_index.get_children().filter(slug='christmas').first()
 
         # Check that the revisions weren't copied
-        self.assertEqual(new_christmas_event.specific.revisions.count(), 0, "Revisions were copied")
+        # Copying creates a new revision so we're expecting the new page to have one revision
+        self.assertEqual(new_christmas_event.specific.revisions.count(), 1)
 
         # Check that the revisions weren't removed from old page
         self.assertEqual(old_christmas_event.specific.revisions.count(), 1, "Revisions were removed from the original page")
 
+    def test_copy_page_updates_user(self):
+        event_moderator = get_user_model().objects.get(username='eventmoderator')
+        christmas_event = EventPage.objects.get(url_path='/home/events/christmas/')
+        christmas_event.save_revision()
+
+        # Copy it
+        new_christmas_event = christmas_event.copy(
+            update_attrs={'title': "New christmas event", 'slug': 'new-christmas-event'},
+            user=event_moderator,
+        )
+
+        # Check that the owner has been updated
+        self.assertEqual(new_christmas_event.owner, event_moderator)
+
+        # Check that the user on the last revision is correct
+        self.assertEqual(new_christmas_event.get_latest_revision().user, event_moderator)
+
 
 class TestSubpageTypeBusinessRules(TestCase):
     def test_allowed_subpage_types(self):