Repository: wagtail/wagtail

Issue:
Crash when moving a page from one parent to another where there is an already existing page with the same slug
### Issue Summary

Hi,
I get a crash when moving a page from a parent to another one where there is already a page with the same slug.

### Steps to Reproduce

1) Create a page P1 with a slug "test"
2) Create another page P2 with same slug under another parent
3) Move P1 onto P2's parent

(traceback wagtaildemo 1.4.1)
```
Traceback:
File "C:\test\wagtaildemo\lib\site-packages\django\core\handlers\base.py" in get_response
  132.                     response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "C:\test\wagtaildemo\lib\site-packages\django\views\decorators\cache.py" in _cache_controlled
  43.             response = viewfunc(request, *args, **kw)
File "C:\test\wagtaildemo\lib\site-packages\wagtail\wagtailadmin\decorators.py" in decorated_view
  22.             return view_func(request, *args, **kwargs)
File "C:\test\wagtaildemo\lib\site-packages\wagtail\wagtailadmin\views\pages.py" in move_confirm
  649.         page_to_move.move(destination, pos='last-child')
File "C:\test\wagtaildemo\lib\site-packages\django\utils\decorators.py" in inner
  145.                     return func(*args, **kwargs)
File "C:\test\wagtaildemo\lib\site-packages\wagtail\wagtailcore\models.py" in move
  1055.         new_self.save()
File "C:\test\wagtaildemo\lib\site-packages\django\utils\decorators.py" in inner
  145.                     return func(*args, **kwargs)
File "C:\test\wagtaildemo\lib\site-packages\wagtail\wagtailcore\models.py" in save
  445.         self.full_clean()
File "C:\test\wagtaildemo\lib\site-packages\wagtail\wagtailcore\models.py" in full_clean
  436.         super(Page, self).full_clean(*args, **kwargs)
File "C:\test\wagtaildemo\lib\site-packages\django\db\models\base.py" in full_clean
  1171.             raise ValidationError(errors)

Exception Type: ValidationError at /admin/pages/24/move/6/confirm/
Exception Value: {u'slug': [u'This slug is already in use']}
```

### Technical details

* Python version: 2.7
* Django version: 1.8

I started having the issue working with wagtail 1.7 with wagtailmodeltranslation. I tried first to understand if the bug was related to wagtalmodeltranslation so I removed it from INSTALLED_APPS and I was still having the issue. Anyway to do things cleaner I tried the steps to reproduce the problem directly and only in wagtaildemo although its version is only 1.4.1. And the issue was still there.

I didin't find other issues relating this problem

Thanks for the help


Discussion:
Thanks for the report! Confirmed on a vanilla install with current Wagtail master.

Resolving patch:
diff --git a/wagtail/admin/views/pages.py b/wagtail/admin/views/pages.py
--- a/wagtail/admin/views/pages.py
+++ b/wagtail/admin/views/pages.py
@@ -754,6 +754,13 @@ def move_confirm(request, page_to_move_id, destination_id):
     if not page_to_move.permissions_for_user(request.user).can_move_to(destination):
         raise PermissionDenied
 
+    if not Page._slug_is_available(page_to_move.slug, destination, page=page_to_move):
+        messages.error(
+            request,
+            _("The slug '{0}' is already in use at the selected parent page. Make sure the slug is unique and try again".format(page_to_move.slug))
+        )
+        return redirect('wagtailadmin_pages:move_choose_destination', page_to_move.id, destination.id)
+
     for fn in hooks.get_hooks('before_move_page'):
         result = fn(request, page_to_move, destination)
         if hasattr(result, 'status_code'):


Test patch:
diff --git a/wagtail/admin/tests/test_pages_views.py b/wagtail/admin/tests/test_pages_views.py
--- a/wagtail/admin/tests/test_pages_views.py
+++ b/wagtail/admin/tests/test_pages_views.py
@@ -2575,22 +2575,29 @@ def setUp(self):
         # Find root page
         self.root_page = Page.objects.get(id=2)
 
-        # Create two sections
+        # Create three sections
         self.section_a = SimplePage(title="Section A", slug="section-a", content="hello")
         self.root_page.add_child(instance=self.section_a)
 
         self.section_b = SimplePage(title="Section B", slug="section-b", content="hello")
         self.root_page.add_child(instance=self.section_b)
 
-        # Add test page into section A
-        self.test_page = SimplePage(title="Hello world!", slug="hello-world", content="hello")
-        self.section_a.add_child(instance=self.test_page)
+        self.section_c = SimplePage(title="Section C", slug="section-c", content="hello")
+        self.root_page.add_child(instance=self.section_c)
+
+        # Add test page A into section A
+        self.test_page_a = SimplePage(title="Hello world!", slug="hello-world", content="hello")
+        self.section_a.add_child(instance=self.test_page_a)
+
+        # Add test page B into section C
+        self.test_page_b = SimplePage(title="Hello world!", slug="hello-world", content="hello")
+        self.section_c.add_child(instance=self.test_page_b)
 
         # Login
         self.user = self.login()
 
     def test_page_move(self):
-        response = self.client.get(reverse('wagtailadmin_pages:move', args=(self.test_page.id, )))
+        response = self.client.get(reverse('wagtailadmin_pages:move', args=(self.test_page_a.id, )))
         self.assertEqual(response.status_code, 200)
 
     def test_page_move_bad_permissions(self):
@@ -2602,19 +2609,32 @@ def test_page_move_bad_permissions(self):
         self.user.save()
 
         # Get move page
-        response = self.client.get(reverse('wagtailadmin_pages:move', args=(self.test_page.id, )))
+        response = self.client.get(reverse('wagtailadmin_pages:move', args=(self.test_page_a.id, )))
 
         # Check that the user received a 403 response
         self.assertEqual(response.status_code, 403)
 
     def test_page_move_confirm(self):
         response = self.client.get(
-            reverse('wagtailadmin_pages:move_confirm', args=(self.test_page.id, self.section_b.id))
+            reverse('wagtailadmin_pages:move_confirm', args=(self.test_page_a.id, self.section_b.id))
         )
         self.assertEqual(response.status_code, 200)
 
+        response = self.client.get(
+            reverse('wagtailadmin_pages:move_confirm', args=(self.test_page_b.id, self.section_a.id))
+        )
+        # Duplicate slugs triggers a redirect with an error message.
+        self.assertEqual(response.status_code, 302)
+
+        response = self.client.get(reverse('wagtailadmin_home'))
+        messages = list(response.context['messages'])
+        self.assertEqual(len(messages), 1)
+        self.assertEqual(messages[0].level, message_constants.ERROR)
+        # Slug should be in error message.
+        self.assertIn("{}".format(self.test_page_b.slug), messages[0].message)
+
     def test_page_set_page_position(self):
-        response = self.client.get(reverse('wagtailadmin_pages:set_page_position', args=(self.test_page.id, )))
+        response = self.client.get(reverse('wagtailadmin_pages:set_page_position', args=(self.test_page_a.id, )))
         self.assertEqual(response.status_code, 200)
 
     def test_before_move_page_hook(self):
@@ -2626,7 +2646,7 @@ def hook_func(request, page, destination):
             return HttpResponse("Overridden!")
 
         with self.register_hook('before_move_page', hook_func):
-            response = self.client.get(reverse('wagtailadmin_pages:move_confirm', args=(self.test_page.id, self.section_b.id)))
+            response = self.client.get(reverse('wagtailadmin_pages:move_confirm', args=(self.test_page_a.id, self.section_b.id)))
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.content, b"Overridden!")
@@ -2640,14 +2660,14 @@ def hook_func(request, page, destination):
             return HttpResponse("Overridden!")
 
         with self.register_hook('before_move_page', hook_func):
-            response = self.client.post(reverse('wagtailadmin_pages:move_confirm', args=(self.test_page.id, self.section_b.id)))
+            response = self.client.post(reverse('wagtailadmin_pages:move_confirm', args=(self.test_page_a.id, self.section_b.id)))
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.content, b"Overridden!")
 
         # page should not be moved
         self.assertEqual(
-            Page.objects.get(id=self.test_page.id).get_parent().id,
+            Page.objects.get(id=self.test_page_a.id).get_parent().id,
             self.section_a.id
         )
 
@@ -2659,14 +2679,14 @@ def hook_func(request, page):
             return HttpResponse("Overridden!")
 
         with self.register_hook('after_move_page', hook_func):
-            response = self.client.post(reverse('wagtailadmin_pages:move_confirm', args=(self.test_page.id, self.section_b.id)))
+            response = self.client.post(reverse('wagtailadmin_pages:move_confirm', args=(self.test_page_a.id, self.section_b.id)))
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.content, b"Overridden!")
 
         # page should be moved
         self.assertEqual(
-            Page.objects.get(id=self.test_page.id).get_parent().id,
+            Page.objects.get(id=self.test_page_a.id).get_parent().id,
             self.section_b.id
         )