Repository: django-cms/django-cms

Issue:
Admin interface: Add page in the top right corner does not always check for duplicate slugs
I wasn't able to find this issue reported elsewhere, and I hope this is not a duplicate or already fixed.

Usually, I get a warning if I add a page with a slug which is already in use.

But I don't if I do this:
1.) In the admin interface, copy a page (I put the copy into the main menu)
2.) change the slug to 'foo'
3.) click on 'add page' in the top right corner
4.) make the new page slug 'foo'
4a.) NO WARNING!
5.) enjoy the Bad Gateway error / MultipleObjectsReturned() error

Regards, 
Moini



Discussion:
+1 got this also on a new project, with customer it is a little risky even if we warn about this bug, they probably will make the error 

Seems related to #3548

@Moini @sveetch could you check on 3.0.10? I can't reproduce this

@yakky : these screenshots are with 3.09 still:
![bildschirmfoto - 22 02 2015 - 22 00 24](https://cloud.githubusercontent.com/assets/3240233/6320504/a6307e02-badf-11e4-8a5c-0e2d346fbe36.png)

![bildschirmfoto - 22 02 2015 - 22 01 17](https://cloud.githubusercontent.com/assets/3240233/6320554/f4eb9e5e-bae0-11e4-8b53-26d2310314d8.png)

After upgrading to 3.10, I tried the (new? didn't see it in 3.09) 'insert above' after clicking on the copy icon in the pages overview, and it didn't insert anything as far as I could see, just like the others, 'insert below/insert inside'... 
I don't get an error message, it's just that nothing happens when I click on any of those arrows. Not even a rumbling of my hard disk (which I can usually hear whenever there's a db access).

Can't reproduce, because I can't use the copy function I used before. Are there any known incompatibilities? Or am I doing something terribly wrong?

Could you try running `manage.py cms fix-mptt`?
The three insert function and copy/paste works and no change related to these functions is in 3.0.10.
My guess is that the mptt tree is has some errors which causes the insert / copy to fail.
You can debug this by opening the inspector and looking at the messages returned by the ajax calls

@yakky , the fix-mptt command returns an error, in one of our own functions which is part of the cms-diff functionality. I don't know enough to give qualified info about that :( . 
I'll ask doctormo if he can help here.

After commenting out our own cms-modifying apps from the settings.py, I was able to run fix-mptt without errors.
I still can't copy pages.
On the terminal, what I see after clicking on one of the arrows is: 
[23/Feb/2015 14:33:45] "POST /en/admin/cms/page/333/dialog/copy/ HTTP/1.1" 200 0

In the debugger in FF, I get this:
![bildschirmfoto - 23 02 2015 - 15 35 07](https://cloud.githubusercontent.com/assets/3240233/6329565/cbc2791c-bb71-11e4-88ea-a3194bd57ade.png)

I'm not sure if that is what you asked me to do, so please direct me if I did the wrong thing. If I had to guess, I'd say the part with 'No response body' is problematic...

@Moini is `CMS_PERMISSION` enabled in the project? If so, do you get a dialog after clicking on the arrow-down icon?

@Moini could you test #3908? that should re-enable copy feature when permission is disabled


Resolving patch:
diff --git a/cms/utils/page.py b/cms/utils/page.py
--- a/cms/utils/page.py
+++ b/cms/utils/page.py
@@ -2,6 +2,7 @@
 from django.conf import settings
 from django.db.models import Q
 import re
+from cms.exceptions import NoHomeFound
 
 APPEND_TO_SLUG = "-copy"
 COPY_SLUG_REGEX = re.compile(r'^.*-copy(?:-(\d+)*)?$')
@@ -10,7 +11,7 @@
 def is_valid_page_slug(page, parent, lang, slug, site, path=None):
     """Validates given slug depending on settings.
     """
-    from cms.models import Title
+    from cms.models import Page, Title
     # Since 3.0 this must take into account unpublished pages as it's necessary
     # to be able to open every page to edit content.
     # If page is newly created (i.e. page.pk is None) we skip filtering out
@@ -25,20 +26,29 @@ def is_valid_page_slug(page, parent, lang, slug, site, path=None):
 
     if parent:
         if parent.is_home:
+            # siblings on hoe and parentless
             qs = qs.filter(Q(page__parent=parent) |
                            Q(page__parent__isnull=True))
         else:
+            # siblings on the same parent page
             qs = qs.filter(page__parent=parent)
     else:
-        qs = qs.filter(page__parent__isnull=True)
+        try:
+            # siblings on home and parentless
+            home = Page.objects.get_home(site)
+            qs = qs.filter(Q(page__parent=home) |
+                           Q(page__parent__isnull=True))
+        except NoHomeFound:
+            # if no home is published, check among parentless siblings only
+            qs = qs.filter(page__parent__isnull=True)
 
     if page.pk:
         qs = qs.exclude(Q(language=lang) & Q(page=page))
         qs = qs.exclude(page__publisher_public=page)
-        # Check for slugs
+    # Check for slugs
     if qs.filter(slug=slug).count():
         return False
-        # Check for path
+    # Check for path
     if path and qs.filter(path=path).count():
         return False
     return True


Test patch:
diff --git a/cms/tests/page.py b/cms/tests/page.py
--- a/cms/tests/page.py
+++ b/cms/tests/page.py
@@ -133,15 +133,17 @@ def test_child_slug_collision(self):
         """
         Test a slug collision
         """
-        create_page("home", 'nav_playground.html', "en")
+        root = create_page("home", 'nav_playground.html', "en", published=True)
         page = create_page("page", 'nav_playground.html', "en")
         subPage = create_page("subpage", 'nav_playground.html', "en", parent=page)
+        create_page("child-page", 'nav_playground.html', "en", parent=root)
         superuser = self.get_superuser()
         with self.login_user_context(superuser):
 
             response = self.client.get(URL_CMS_PAGE_ADD+"?target=%s&position=right&site=1" % subPage.pk)
             self.assertContains(response, 'value="%s"' % page.pk)
 
+            # slug collision between two child pages of the same node
             page_data = self.get_new_page_data(page.pk)
             page_data['slug'] = 'subpage'
             response = self.client.post(URL_CMS_PAGE_ADD, page_data)
@@ -150,6 +152,24 @@ def test_child_slug_collision(self):
             self.assertTrue(response.request['PATH_INFO'].endswith(URL_CMS_PAGE_ADD))
             self.assertContains(response, '<ul class="errorlist"><li>Another page with this slug already exists</li></ul>')
 
+            # slug collision between page with no parent and a child page of root
+            page_data = self.get_new_page_data()
+            page_data['slug'] = 'child-page'
+            response = self.client.post(URL_CMS_PAGE_ADD, page_data)
+
+            self.assertEqual(response.status_code, 200)
+            self.assertTrue(response.request['PATH_INFO'].endswith(URL_CMS_PAGE_ADD))
+            self.assertContains(response, '<ul class="errorlist"><li>Another page with this slug already exists</li></ul>')
+
+            # slug collision between two top-level pages
+            page_data = self.get_new_page_data()
+            page_data['slug'] = 'page'
+            response = self.client.post(URL_CMS_PAGE_ADD, page_data)
+
+            self.assertEqual(response.status_code, 200)
+            self.assertTrue(response.request['PATH_INFO'].endswith(URL_CMS_PAGE_ADD))
+            self.assertContains(response, '<ul class="errorlist"><li>Another page with this slug already exists</li></ul>')
+
     def test_get_available_slug_recursion(self):
         """ Checks cms.utils.page.get_available_slug for infinite recursion
         """
@@ -181,9 +201,9 @@ def test_slug_collisions_api_2(self):
                               published=True, parent=page1, slug="foo")
         page2 = create_page('test page 1_1', 'nav_playground.html', 'en',
                             published=True, slug="foo")
-        # Home page child has an invalid slug, while root page is ok. Root wins!
+        # Root (non home) page and child page has the same slug, both are invalid
         self.assertFalse(is_valid_page_slug(page1_1, page1_1.parent, "en", page1_1.get_slug("en"), page1_1.site))
-        self.assertTrue(is_valid_page_slug(page2, page2.parent, "en", page2.get_slug("en"), page2.site))
+        self.assertFalse(is_valid_page_slug(page2, page2.parent, "en", page2.get_slug("en"), page2.site))
 
     def test_slug_collisions_api_3(self):
         """ Checks for slug collisions on children of a non root page - uses API to create pages