Repository: CTPUG/wafer

Issue:
Duplicate page created
On https://wafertest.debconf.org, I created the following page: https://wafertest.debconf.org/debconf-16-bursaries-instructions, when my Wafer pages page loaded, I saw that there existed two new pages with that title.

When I visit https://wafertest.debconf.org/debconf-16-bursaries-instructions, wafer gives me a debug page that says "get() returned more than one Page -- it returned 2!"

Here is the traceback: http://paste.debian.net/415666/

For now I'll just delete the duplicate page, but @stefanor  mentioned that a unique index for pages may be required.



Discussion:
This is a good idea, although page parentage might make it a little complicated.

For what it's worth, this only happened for the first page, I created another page to test if it would happen again and just a single page was created as expected, so perhaps that was just some freak case, I'll keep an eye out for it if it happens again.

At the moment, there's no check to prevent creating duplicate pages at all, so any glitch that causes the admin form to be submitted twice will result in this behaviour.

Having all the page names and/or page slugs be unique could be done, but breaks some legimate use cases (allowing sprints/venue and talks/venue seems useful, for example). We should however ensure that that the <parent>/slug combination is always unique. 

We should add list the parent in the admin interface, since the current listing can be confusing when pages with the same name but different parents exist in the database.

Hrm, it looks like we already try to enforce this.
https://github.com/CTPUG/wafer/blob/4c0aa65571d45e9190a2ba237e49e6cfac2d2809/wafer/pages/models.py#L86

I'm guessing the problem is the null parent? Maybe the index should have slug first? I don't know how compound indexes including NULLs behave...

SQL unique constraints and foreign keys are one of the grey areas of the standard and thus don't mix particularly well, IIRC, so we can't just rely on the constraint to enforce this.


Resolving patch:
diff --git a/wafer/pages/admin.py b/wafer/pages/admin.py
--- a/wafer/pages/admin.py
+++ b/wafer/pages/admin.py
@@ -7,7 +7,8 @@
 
 class PageAdmin(CompareVersionAdmin, admin.ModelAdmin):
     prepopulated_fields = {"slug": ("name",)}
-    list_display = ('name', 'slug', 'get_people_display_names', 'get_in_schedule')
+    list_display = ('name', 'slug', 'get_absolute_url',
+                    'get_people_display_names', 'get_in_schedule')
 
     list_filter = (DateModifiedFilter,)
 
diff --git a/wafer/pages/models.py b/wafer/pages/models.py
--- a/wafer/pages/models.py
+++ b/wafer/pages/models.py
@@ -3,6 +3,7 @@
 
 from django.utils.translation import ugettext_lazy as _
 from django.core.urlresolvers import reverse
+from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
 from django.conf import settings
 from django.db import models
 from django.db.models.signals import post_save
@@ -63,6 +64,8 @@ def get_absolute_url(self):
         url = "/".join(self.get_path())
         return reverse('wafer_page', args=(url,))
 
+    get_absolute_url.short_description = 'page url'
+
     def get_in_schedule(self):
         if self.scheduleitem_set.all():
             return True
@@ -85,6 +88,35 @@ def get_people_display_names(self):
     class Model:
         unique_together = (('parent', 'slug'),)
 
+    def clean(self):
+        keys = [self.pk]
+        parent = self.parent
+        while parent is not None:
+            if parent.pk in keys:
+                raise ValidationError(
+                    {
+                        NON_FIELD_ERRORS: [
+                            _("Circular reference in parent."),
+                        ],
+                    })
+            keys.append(parent.pk)
+            parent = parent.parent
+        return super(Page, self).clean()
+
+    def validate_unique(self, exclude=None):
+        existing = Page.objects.filter(slug=self.slug, parent=self.parent)
+        # We could be updating the page, so don't fail if the existing
+        # entry is this page.
+        if existing.count() > 1 or (existing.count() == 1 and
+                                    existing.first().pk != self.pk):
+            raise ValidationError(
+                {
+                    NON_FIELD_ERRORS: [
+                        _("Duplicate parent/slug combination."),
+                    ],
+                })
+        return super(Page, self).validate_unique(exclude)
+
 
 def page_menus(root_menu):
     """Add page menus."""


Test patch: